# Vikunja: Scoped API token can mint unrestricted OAuth session credentials

A Vikunja API token scoped only to oauth.authorize could mint a full OAuth JWT and refresh token, escaping its permission boundary.

- Payout: Research
- Severity: high
- Program: Vikunja
- Found by: baradika
- Disclosed: 2026-10-09
- CVE: CVE-2026-57458
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-v3p6-34mc-hj7v
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-v3p6-34mc-hj7v
- Tags: area:web domain:api domain:web impact:integrity kind:research platform:ghsa sev:high tech:go tech:oauth technique:source-review vuln:access-control vuln:auth-bypass vuln:privilege-escalation

**Target:** OAuth authorization/token endpoints (/api/v1/oauth/authorize, /api/v1/oauth/token)
**Root cause:** The OAuth authorize endpoint accepts an API-token-authenticated user context as a valid resource owner, and the issued authorization code is not bound to the original token's scopes, so the exchanged JWT carries full user permissions.
**Steps:**
1. Create API token with permission `{"oauth":["authorize"]}` (201 Created).
2. Negative control: `GET /api/v1/user` with `Authorization: Bearer <scoped-api-token>` returns 401.
3. `POST /api/v1/oauth/authorize` with the scoped token and body `{"response_type":"code","client_id":"vikunja","redirect_uri":"vikunja-flutter://callback","code_challenge":"<pkce-s256-challenge>","code_challenge_method":"S256"}` returns 200 with an authorization `code`.
4. `POST /api/v1/oauth/token` with `{"grant_type":"authorization_code","code":"<code>","client_id":"vikunja","redirect_uri":"vikunja-flutter://callback","code_verifier":"<verifier>"}` returns 200 with `access_token` and `refresh_token`.
5. `GET /api/v1/user` and `GET /api/v1/projects` with the minted bearer token return 200.
**How found:** Noticed that the OAuth authorize route was registered in an authenticated API route group that also accepts scoped API tokens, and that api_user from token auth is treated as the current user by the OAuth server; tested whether a token scoped only to oauth.authorize could complete the authorization-code flow.
**Exploitation:** An attacker holding or delegated a narrowly scoped API token (oauth.authorize only) calls the OAuth authorize endpoint with a self-controlled PKCE challenge, receives an authorization code, exchanges it at the token endpoint for a normal bearer JWT plus refresh token, then uses those credentials on routes outside the original token scope.
**Escalation:** Minted refresh token allows continued access beyond the 600s access-token lifetime; no cross-user or admin escalation validated.
**Insight:** When an app exposes both scoped API tokens and an OAuth authorization-code flow, check whether the authorize endpoint accepts token-authenticated contexts as resource owners and whether issued codes inherit the token's scopes; a scope-limited token that can mint a session is a full scope escape.

