Summary
internal/middleware/scope.go::RequireScopes short-circuits with ctx.Next() whenever the JWT typ is session. Every /api/login token is a session token. None of the affected handlers (GetSystemLogs, GetVisitorStats, CheckUpdate, GetAllUsers, WSSubscribeSystemLogs) does its own IsAdmin check, and /ws/system/logs is registered under WSRouterGroup, which has no middleware at all. Result: any logged-in non-admin reads system logs (HTTP and live WebSocket), visitor stats, update info, and every other user's email. The fix in v4.4.3 (commit 6df56730) only added RequireScopes, which doesn't apply to session tokens, so the original advisory PoCs still succeed in v4.5.0.
Details
Three independent gaps, all required for the bypass:
internal/middleware/scope.go:14-22 — RequireScopes returns Next() immediately for session tokens, before checking scope/audience. The intent (PR #195: "session = interactive UI") is fine if admin enforcement moves to handlers; it didn't.
- No
IsAdmin check in the affected handlers/services:
internal/handler/dashboard/dashboard.go::GetSystemLogs / GetVisitorStats / CheckUpdate / WSSubscribeSystemLogs / SSESubscribeSystemLogs
internal/service/user/user.go::GetAllUsers (line 405)
grep -rn "IsAdmin" internal/handler/dashboard/ internal/service/dashboard/ → 0 hits. Other functions in the same user.go file (UpdateUser, UpdateUserAdmin, DeleteUser) check IsAdmin/IsOwner explicitly, so this is missed-coverage, not intentional.
internal/router/router.go:38 — ws := r.Group("/ws") has no .Use(...) anywhere. The WSSubscribeSystemLogs handler only calls jwtUtil.ParseToken(token) and never verifies IsAdmin / scope / audience. CheckOrigin is also return true, enabling Cross-Site WebSocket Hijacking.
The v4.4.3 patch (commit 6df56730) added RequireScopes(ScopeAdminSettings) to /api/system/logs and /api/system/logs/stream only. Both advisories' published PoCs used a session token, which still passes RequireScopes, so the patch is structurally ineffective. /ws/system/logs was never modified.
PoC
poc.zip
Please extract the uploaded compressed file before proceeding
- docker compose up -d --build
- ./poc.sh
- python3 poc_ws.py
Impact
- Live system log tail for any logged-in user via
/ws/system/logs and /api/system/logs[/stream]: failed-login username enumeration, denied-action stack traces with internal CI paths (/home/runner/work/Ech0/Ech0/...) and full Gin middleware chain, audit trail of every admin action.
- PII:
/api/users returns username + email + admin flag for every non-owner user to any logged-in user.
Summary
internal/middleware/scope.go::RequireScopesshort-circuits withctx.Next()whenever the JWTtypissession. Every/api/logintoken is a session token. None of the affected handlers (GetSystemLogs,GetVisitorStats,CheckUpdate,GetAllUsers,WSSubscribeSystemLogs) does its ownIsAdmincheck, and/ws/system/logsis registered underWSRouterGroup, which has no middleware at all. Result: any logged-in non-admin reads system logs (HTTP and live WebSocket), visitor stats, update info, and every other user's email. The fix in v4.4.3 (commit6df56730) only addedRequireScopes, which doesn't apply to session tokens, so the original advisory PoCs still succeed in v4.5.0.Details
Three independent gaps, all required for the bypass:
internal/middleware/scope.go:14-22—RequireScopesreturnsNext()immediately for session tokens, before checking scope/audience. The intent (PR #195: "session = interactive UI") is fine if admin enforcement moves to handlers; it didn't.IsAdmincheck in the affected handlers/services:internal/handler/dashboard/dashboard.go::GetSystemLogs / GetVisitorStats / CheckUpdate / WSSubscribeSystemLogs / SSESubscribeSystemLogsinternal/service/user/user.go::GetAllUsers(line 405)grep -rn "IsAdmin" internal/handler/dashboard/ internal/service/dashboard/→ 0 hits. Other functions in the sameuser.gofile (UpdateUser,UpdateUserAdmin,DeleteUser) checkIsAdmin/IsOwnerexplicitly, so this is missed-coverage, not intentional.internal/router/router.go:38—ws := r.Group("/ws")has no.Use(...)anywhere. TheWSSubscribeSystemLogshandler only callsjwtUtil.ParseToken(token)and never verifiesIsAdmin/ scope / audience.CheckOriginis alsoreturn true, enabling Cross-Site WebSocket Hijacking.The v4.4.3 patch (commit
6df56730) addedRequireScopes(ScopeAdminSettings)to/api/system/logsand/api/system/logs/streamonly. Both advisories' published PoCs used a session token, which still passesRequireScopes, so the patch is structurally ineffective./ws/system/logswas never modified.PoC
poc.zip
Please extract the uploaded compressed file before proceeding
Impact
/ws/system/logsand/api/system/logs[/stream]: failed-login username enumeration, denied-action stack traces with internal CI paths (/home/runner/work/Ech0/Ech0/...) and full Gin middleware chain, audit trail of every admin action./api/usersreturns username + email + admin flag for every non-owner user to any logged-in user.