#690·go-admin

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

Author: geo-chenCreated Sep 14, 2026Updated Sep 14, 2026
Labels🐛bug

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 da3ce351e5b7256e3d2745d8f11067d738145182)

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():

go
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():

go
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.