GHSA-v3p6-34mc-hj7v: High severity go/code.vikunja.io/api vulnerability
Summary
A scoped API token can bypass its declared permissions by using the OAuth authorization flow to mint normal session credentials.
A token limited to:
json {"oauth":["authorize"]}
can call /api/v1/oauth/authorize, receive an OAuth authorization code, exchange it at /api/v1/oauth/token, and obtain a normal bearer JWT plus refresh token. The resulting credentials are not restricted by the original API-token permissions.
Affected Component
Scoped API tokens OAuth authorization flow POST /api/v1/oauth/authorize POST /api/v1/oauth/token
Impact
A narrowly scoped API token with only oauth.authorize can be converted into normal session credentials for the same user.
This bypasses the intended API-token permission boundary. An attacker who obtains or is delegated such a limited token can mint a normal JWT and refresh token, then access routes outside the original token scope.
Runtime validation showed that the original scoped token could not access GET /api/v1/user, but the minted OAuth access token could access:
GET /api/v1/user GET /api/v1/projects
No cross-user access, privilege escalation to admin, or access to another account was validated.
Technical Details
The issue is caused by an authentication-context mismatch between scoped API-token authentication and OAuth authorization-code issuance.
The vulnerable chain is:
1. A scoped API token authenticates the request and sets the current user context. 2. /api/v1/oauth/authorize accepts that API-token-authenticated user as a valid OAuth resource owner. 3. The OAuth authorization endpoint issues an authorization code. 4. /api/v1/oauth/token exchanges that code for a normal bearer JWT and refresh token. 5. The resulting credentials are not bound to the original API-token permissions.
Relevant code paths:
pkg/routes/routes.go
registers /api/v1/oauth/authorize in an authenticated API route group that also accepts scoped API tokens
pkg/models/apiroutes.go
exposes non-CRUD subroutes as API-token permissions exposes the OAuth authorization endpoint under the oauth permission group
pkg/routes/apitokens.go
stores apitoken and apiuser in the request context during API-token authentication
pkg/user/user.go
treats apiuser as an authenticated current user
pkg/modules/auth/oauth2server/authorize.go
uses the current user and issues an OAuth authorization code
pkg/modules/auth/oauth2server/token.go
exchanges the authorization code for a normal JWT and refresh token
Steps to Reproduce
1. Create a scoped API token
Create an API token with only the following permission:
json {"oauth":["authorize"]}
Observed response:
http 201 Created
The returned token had only the oauth.authorize permission.
2. Negative control: direct access with scoped token fails
Request:
http GET /api/v1/user Authorization: Bearer <scoped-api-token>
Observed response:
http 401 Unauthorized
Response body:
json {"code":11,"message":"missing, malformed, expired or otherwise invalid token provided"}
This confirms that the scoped token cannot directly access the normal user route.
3. Obtain an OAuth authorization code with the scoped token
Request:
http POST /api/v1/oauth/authorize Authorization: Bearer <scoped-api-token> Content-Type: application/json
Body:
json { "responsetype": "code", "clientid": "vikunja", "redirecturi": "vikunja-flutter://callback", "codechallenge": "<pkce-s256-challenge>", "codechallengemethod": "S256" }
Observed response:
http 200 OK
Response body:
json { "code": "<redacted>", "redirecturi": "vikunja-flutter://callback", "state": "" }
The scoped API token successfully obtained an OAuth authorization code.
4. Exchange the authorization code for session credentials
Request:
http POST /api/v1/oauth/token Content-Type: application/json
Body:
json { "granttype": "authorizationcode", "code": "<redacted>", "clientid": "vikunja", "redirecturi": "vikunja-flutter://callback", "codeverifier": "<original-pkce-verifier>" }
Observed response:
http 200 OK
Response body:
json { "accesstoken": "<redacted>", "tokentype": "bearer", "expiresin": 600, "refreshtoken": "<redacted>" }
The authorization code was exchanged for a normal access token and refresh token.
5. Use the minted access token on normal routes
Request:
http GET /api/v1/user Authorization: Bearer <minted-oauth-access-token>
Observed response:
http 200 OK
Additional scope check:
http GET /api/v1/projects Authorization: Bearer <minted-oauth-access-token>
Observed response:
http 200 OK
The minted OAuth access token could access normal non-OAuth routes that the original scoped API token could not access.
Expected Behavior
A scoped API token should not be able to obtain credentials with broader permissions than its declared scope.
/api/v1/oauth/authorize should require a normal user session or another authentication context suitable for OAuth authorization-code issuance. API-token-authenticated requests should not be accepted for minting OAuth authorization codes.
Actual Behavior
A token scoped only to oauth.authorize can obtain an OAuth authorization code and exchange it for a normal JWT plus refresh token.
The minted credentials are not restricted by the original API-token permissions.
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
go/code.vikunja.io/apito a version that resolves this vulnerability.Fixed in 2.4.0 - Configuration
Require a normal user session or another authentication context suitable for OAuth authorization-code issuance, and do not accept API-token-authenticated requests for minting OAuth authorization codes.
OAuth authorization endpoint (/api/v1/oauth/authorize) authentication context = require a normal user session; reject API-token-authenticated requests
Event History
Frequently Asked Questions
What does an attacker need to exploit this issue?
The attacker needs a scoped API token for the target user that includes the oauth.authorize permission. They can use that token with the OAuth authorization and token endpoints to mint a bearer JWT and refresh token.
What access can the minted credentials provide?
The minted OAuth access token is not constrained by the original API-token scope. Validation confirmed access to /api/v1/user and /api/v1/projects even though the original scoped token could not access /api/v1/user.
Does the issue allow access to other users or administrative privileges?
No cross-user access, escalation to administrator privileges, or access to another account was validated. The disclosed impact is conversion of a limited token into unrestricted normal session credentials for the same user.
What remediation references are provided?
The provided references include commit 4ae2e093014881052ed8f8ecd8bcb9adf83dd276 and the v2.4.0 release tag.