Skip to content

Authorization bypass via unanchored logout-path regex checked against full request URL (including query string) #690

Description

@geo-chen

reported via email on 3 August 2026:

I'm reporting the following vulnerability:

https://github.com/GoAdminGroup/go-admin

Affected Versions: v1.2.26 (HEAD da3ce35)

Summary

go-admin's per-request permission check special-cases the logout endpoint
using an unanchored regular expression matched with substring semantics
against the user's request. Because the value passed into that check is the
full request URL string (path plus query string), not the routed path
alone, any authenticated user, including one with no meaningful permissions
assigned, can bypass every permission check in the framework simply by
appending a query parameter whose value contains the configured admin
prefix followed by "/logout" (e.g. ?ref=/admin/logout) to any admin URL.
The router still dispatches the request to the real target handler based on
the URL path, while the permission check sees the whole path+query string,
matches the logout substring, and unconditionally grants access.

This affects both read-only pages (e.g. the built-in user-management table)
and state-changing actions (e.g. deleting a user), since both go through
the same middleware.

Details

modules/auth/middleware.go, Filter():

func Filter(ctx *context.Context, conn db.Connection) (models.UserModel, bool, bool) {
    ...
    return user, true, CheckPermissions(user, ctx.Request.URL.String(), ctx.Method(), ctx.PostForm())
}

ctx.Request.URL.String() is Go's standard net/url.URL.String(), which
reconstructs the full URL in path?query form, not just the path.

plugins/admin/models/user.go, CheckPermissionByUrlMethod():

func (t UserModel) CheckPermissionByUrlMethod(path, method string, formParams url.Values) bool {

    if t.IsSuperAdmin() {
        return true
    }

    if path == "" {
        return false
    }

    logoutCheck, _ := regexp.Compile(config.Url("/logout") + "(.*?)")

    if logoutCheck.MatchString(path) {
        return true
    }
    ... // real permission-matching logic follows
}

config.Url("/logout") prepends the configured admin URL prefix (e.g.
/admin), producing a pattern like /admin/logout(.*?). This pattern has
no ^/$ anchors, and MatchString performs a substring search anywhere
in the input. Combined with the caller passing the entire URL (path and
query string together) as path, any request whose URL contains the
substring /admin/logout anywhere, including inside an arbitrary query
parameter value, causes this function to return true before the real
permission rules for that user are ever evaluated. The router itself
dispatches purely on URL.Path, so appending a query string does not
change which handler runs; it only changes what the permission check sees.

PoC

(available upon request)

Impact

Any authenticated go-admin user, regardless of how restrictive their
assigned role/permissions are (including a role scoped to a single
dashboard page with no write access at all), can perform any action
reachable through the admin panel by appending a crafted, otherwise
meaningless query parameter to the request. Because the check is applied in
the shared auth middleware used for every protected route (menu
management, table CRUD, user management, exports, etc.), this is not
limited to one endpoint. Confirmed impact includes both unauthorized data
disclosure (viewing the superadmin-only user management page and its data)
and unauthorized data destruction (deleting another user's account via the
framework's own delete endpoint). Since go-admin's own user/role/permission
management pages are themselves reachable this way, a restricted user can
also use the bypass to grant themselves superadmin permissions directly
(e.g. by modifying role_permissions rows through the exposed management
UI), making this a full privilege escalation to superadmin-equivalent
capability from any authenticated low-privilege account.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    🐛bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions