GHSA-x4rv-c3wx-4cr8: SSRF
Summary
Authlib's RFC 9101 (JAR) extension passes the attacker-controlled requesturi authorization parameter straight to the getrequestobject() fetch hook with no validation, at a pre-authentication endpoint. Authlib already defines the pre-registration allowlist for this (requesturis client metadata and requirerequesturiregistration server metadata) but the extension never consults it, and both docstrings instruct integrators to implement the hook as an unrestricted requests.get(requesturi). 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/authorizationserver.py.
The extension registers itself on the beforegetauthorizationgrant hook (lines 62-65), so it runs on every authorization request before any user or client authentication. supportrequesturi defaults to True (line 58).
The only gate before the sink is that clientid names an existing client (lines 91-103). A clientid is a public, non-secret identifier.
The sink at line 159 forwards the raw parameter with no scheme, host, or allowlist check:
python def getrawrequestobject(self, request: OAuth2Request) -> str: if "requesturi" in request.payload.data: rawrequestobject = self.getrequestobject( request.payload.data["requesturi"] ) if not rawrequestobject: raise InvalidRequestUriError(state=request.payload.state)
getrequestobject 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 getrequestobject(self, requesturi: str): try: return requests.get(requesturi).text except requests.Exception: return None
Two points sharpen this beyond a generic documentation issue:
- The allowlist already exists and is ignored. Authlib validates requesturis as client metadata (authlib/oauth2/rfc7591/claims.py:319) and requirerequesturiregistration 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 requesturi 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 requesturi, and d) not perform recursive GET on the requesturi." 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 (invalidrequesturi, lines 162-163), while a successful fetch of non-JWT content raises InvalidRequestObjectError (invalidrequestobject, 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 getrequestobject(self, requesturi): reached.append(requesturi) # the docstring puts requests.get(requesturi) here return None
def resolveclientpublickey(self, client): return {"keys": []}
def getservermetadata(self): return {}
def getclientrequiresignedrequestobject(self, client): return False
class AuthorizationServer: def queryclient(self, clientid): return object()
uri = "http://169.254.169.254/latest/meta-data/" request = OAuth2Request("GET", "https://as.example/authorize") request.payload = BasicOAuth2Payload({"clientid": "demo", "requesturi": uri})
try: Extension().parseauthorizationrequest(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?responsetype=code&clientid=demo&requesturi=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 clientid. In a deployment that implemented getrequestobject 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 invalidrequesturi versus invalidrequestobject 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 supportrequesturi=True, and its documentation steers integrators to the unsafe implementation, so the insecure outcome is the expected one.
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
pip/authlibto a version that resolves this vulnerability.Fixed in 1.9.0 - Compensating control
Before invoking the get_request_object fetch hook, enforce the registered request URI allowlist using the client's request_uris metadata and the server's require_request_uri_registration metadata; reject request_uri values that are not registered or otherwise point to an unexpected location.
- Compensating control
For request_uri retrieval, verify that the response media type is application/oauth-authz-req+jwt, enforce a timeout when obtaining the content, and do not perform recursive GET requests on request_uri.