# OAuthLib: Token grants can use attacker-supplied client_id for authorization after authenticating a different client

OAuthlib token grants authorize grant type and scopes using attacker-supplied body client_id instead of the authenticated client, letting a low-privilege client obtain admin-scoped tokens.

- Payout: Research
- Severity: high
- Program: OAuthlib
- Found by: Str1ckl4nd
- Disclosed: 2026-10-30
- CVE: CVE-2026-118669
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-7984-qgvg-3j9c
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-7984-qgvg-3j9c
- Tags: area:web domain:oss domain:web impact:privesc kind:research platform:ghsa sev:high tech:oauth tech:python technique:source-review vuln:access-control vuln:auth-bypass

**Target:** OAuth2 token endpoint (client_credentials and password grants)
**Root cause:** After authenticate_client() sets request.client, OAuthlib only fills request.client_id when missing, so an attacker-supplied body client_id survives and is passed to validate_grant_type()/validate_scopes() while the authenticated client object differs.
**Steps:**
1. Install oauthlib==3.3.1 and build a TokenEndpoint with ClientCredentialsGrant and ResourceOwnerPasswordCredentialsGrant over a validator that authorizes by the client_id argument.
2. Authenticate as low-privilege client via `Authorization: Basic base64(low:low-secret)`.
3. Control: send `grant_type=client_credentials&client_id=low&scope=admin` -> 400 invalid_scope.
4. Spoof: send `grant_type=client_credentials&client_id=high&scope=admin` -> 200 with `scope=admin`, while request.client.client_id stays 'low'.
5. Repeat with `grant_type=password&client_id=high&username=alice&password=wonderland&scope=admin` -> 200 with admin scope and refresh_token.
**How found:** Code reading of the grant handlers revealed the ordering: validate_grant_type/validate_scopes run before request.client_id is normalized from request.client.client_id, and the `or` fallback preserves any body-supplied client_id, creating a split identity state.
**Exploitation:** Attacker with valid credentials for one low-privilege confidential client authenticates normally, then injects another client's client_id into the token request body; OAuthlib validates grant type and scopes against the spoofed id, issuing tokens with scopes/grants not assigned to the authenticated client.
**Insight:** In OAuth libraries, check whether authorization hooks receive the authenticated principal or a request-parsed identifier; any `x = x or authenticated.x` normalization after authorization calls is a spoofing window.

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

**Spoofed token requests with client_id=high**

```
assert_rejected(
    "control client_credentials as low requesting admin",
    "grant_type=client_credentials&client_id=low&scope=admin",
)
assert_spoofed_success(
    "spoofed client_credentials as low with client_id=high",
    "grant_type=client_credentials&client_id=high&scope=admin",
)

assert_rejected(
    "control password as low requesting admin",
    "grant_type=password&client_id=low&username=alice&password=wonderland&scope=admin",
)
assert_spoofed_success(
    "spoofed password as low with client_id=high",
    "grant_type=password&client_id=high&username=alice&password=wonderland&scope=admin",
)
```

