{"id":"CVE-2026-114620","aliases":["GHSA-x4rv-c3wx-4cr8"],"title":"Authlib: Unvalidated request_uri fetch in JWT-secured authorization request (JAR) leading to SSRF","summary":"Authlib: Unvalidated request_uri fetch in JWT-secured authorization request (JAR) leading to SSRF","severity":"medium","cvss":5.4,"cvssVector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:L/I:L/A:N","vendor":"authlib","product":"authlib","ecosystem":"pip","affected":["authlib >= 1.6.0, < 1.9.0"],"patched":["authlib 1.9.0"],"published":"2026-10-27","updated":"2026-10-27","sourceUpdated":"2026-10-27T15:47:25.081805715Z","source":"OSV","sourceUrl":"https://osv.dev/vulnerability/GHSA-x4rv-c3wx-4cr8","references":[{"url":"https://github.com/authlib/authlib/security/advisories/GHSA-x4rv-c3wx-4cr8"},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-114620"},{"url":"https://github.com/authlib/authlib/pull/949"},{"url":"https://github.com/authlib/authlib/commit/6563f6f7a8c2f71d2f85ee7511d72a4fbae3acc0"},{"url":"https://github.com/authlib/authlib/commit/891e3f10ae5989176211f8be35dcc7cd40aa92c6"},{"url":"https://github.com/authlib/authlib"},{"url":"https://github.com/authlib/authlib/releases/tag/v1.9.0"},{"url":"https://github.com/advisories/GHSA-x4rv-c3wx-4cr8"},{"url":"https://security.access.redhat.com/data/csaf/v2/vex/2026/cve-2026-114620.json"},{"url":"https://access.redhat.com/security/cve/CVE-2026-114620"},{"url":"https://bugzilla.redhat.com/show_bug.cgi?id=2559403"},{"url":"https://www.cve.org/CVERecord?id=CVE-2026-114620"}],"tags":["osv","pip","ghsa","csaf","vex","red-hat","cve.org"],"epss":0.0029,"epssPercentile":0.21459,"cwe":["CWE-918"],"scores":{"osv":5.4,"ghsa":5.4,"vendor":4.8,"cna":5.4},"ingestedAt":"2026-10-22T22:00:50.450Z","slug":"CVE-2026-114620","body":"## Overview\n\n### Summary\n\nAuthlib'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. \n### Details\n\nVerified against authlib 1.7.2 and current `main`. All line numbers below are from `authlib/oauth2/rfc9101/authorization_server.py`.\n\nThe 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).\n\nThe 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.\n\nThe sink at line 159 forwards the raw parameter with no scheme, host, or allowlist check:\n\n```python\ndef _get_raw_request_object(self, request: OAuth2Request) -> str:\n    if \"request_uri\" in request.payload.data:\n        raw_request_object = self.get_request_object(\n            request.payload.data[\"request_uri\"]\n        )\n        if not raw_request_object:\n            raise InvalidRequestUriError(state=request.payload.state)\n```\n\n`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):\n\n```python\ndef get_request_object(self, request_uri: str):\n    try:\n        return requests.get(request_uri).text\n    except requests.Exception:\n        return None\n```\n\nTwo points sharpen this beyond a generic documentation issue:\n\n- **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.\n- **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.\n\nThe 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).\n\n### PoC\n\nThis demonstrates the library-level defect with no network egress required: it shows the unvalidated URI reaching the fetch hook pre-authentication.\n\n```bash\npython -m venv .venv && .venv/bin/pip install authlib\n```\n\n```python\nfrom authlib.oauth2.rfc6749.requests import BasicOAuth2Payload, OAuth2Request\nfrom authlib.oauth2.rfc9101 import JWTAuthenticationRequest\n\nreached = []\n\n\nclass Extension(JWTAuthenticationRequest):\n    def get_request_object(self, request_uri):\n        reached.append(request_uri)  # the docstring puts requests.get(request_uri) here\n        return None\n\n    def resolve_client_public_key(self, client):\n        return {\"keys\": []}\n\n    def get_server_metadata(self):\n        return {}\n\n    def get_client_require_signed_request_object(self, client):\n        return False\n\n\nclass AuthorizationServer:\n    def query_client(self, client_id):\n        return object()\n\n\nuri = \"http://169.254.169.254/latest/meta-data/\"\nrequest = OAuth2Request(\"GET\", \"https://as.example/authorize\")\nrequest.payload = BasicOAuth2Payload({\"client_id\": \"demo\", \"request_uri\": uri})\n\ntry:\n    Extension().parse_authorization_request(AuthorizationServer(), request)\nexcept Exception as exc:\n    print(\"error:\", type(exc).__name__)\n\nprint(\"fetch hook received:\", reached)\n```\n\nOutput on authlib 1.7.2:\n\n```text\nerror: InvalidRequestUriError\nfetch hook received: ['http://169.254.169.254/latest/meta-data/']\n```\n\nAgainst a deployment that used the docstring implementation, the equivalent unauthenticated request is:\n\n```text\nGET /authorize?response_type=code&client_id=demo&request_uri=http%3A%2F%2F169.254.169.254%2Flatest%2Fmeta-data%2F\n```\n\nReturning 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.\n\n### Impact\n\nServer-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.\n\nResponse 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.\n\n## Affected packages\n\n- `authlib >= 1.6.0, < 1.9.0`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `authlib 1.9.0`\n\n## Vendor advisories\n\n- **Red Hat VEX** · Moderate · affected: Exploit Intelligence, Lightspeed Core, Migration Toolkit for Applications 8, OpenShift Lightspeed, Red Hat Ansible Automation Platform 2, Red Hat Enterprise Linux command line assistant, … · no fix planned: Exploit Intelligence, Red Hat Quay 3, Lightspeed Core, Migration Toolkit for Applications 8, … · updated 2026-10-23 · [vex](https://security.access.redhat.com/data/csaf/v2/vex/2026/cve-2026-114620.json)","depth":"sunlit","depthScore":30,"depthScoreParts":{"impact":29.7,"likelihood":0.1,"exploitation":0,"ransomware":0},"changes":[{"seq":229841,"id":"CVE-2026-114620","ts":1793117556277,"field":"cvss","old":"4.8","new":"5.4"},{"seq":228239,"id":"CVE-2026-114620","ts":1792758227203,"field":"cvss","old":"5.4","new":"4.8"}]}