GHSA-X4RV-C3WX-4CR8

Vulnerability from github – Published: 2026-10-27 15:39 – Updated: 2026-10-27 15:39
VLAI
Summary
Authlib: Unvalidated request_uri fetch in JWT-secured authorization request (JAR) leading to SSRF
Details

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:

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_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.

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.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "authlib"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "1.6.0"
            },
            {
              "fixed": "1.9.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-114620"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-10-27T15:39:18Z",
    "nvd_published_at": "2026-10-22T22:18:05Z",
    "severity": "MODERATE"
  },
  "details": "### Summary\n\nAuthlib\u0027s 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) -\u003e 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 \u2014 in the class docstring (lines 34-38) and in the hook\u0027s 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 \u00a710.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 \u0026\u0026 .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: [\u0027http://169.254.169.254/latest/meta-data/\u0027]\n```\n\nAgainst a deployment that used the docstring implementation, the equivalent unauthenticated request is:\n\n```text\nGET /authorize?response_type=code\u0026client_id=demo\u0026request_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 \u00a710.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 \u2014 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.",
  "id": "GHSA-x4rv-c3wx-4cr8",
  "modified": "2026-10-27T15:39:18Z",
  "published": "2026-10-27T15:39:18Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/authlib/authlib/security/advisories/GHSA-x4rv-c3wx-4cr8"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-114620"
    },
    {
      "type": "WEB",
      "url": "https://github.com/authlib/authlib/pull/949"
    },
    {
      "type": "WEB",
      "url": "https://github.com/authlib/authlib/commit/6563f6f7a8c2f71d2f85ee7511d72a4fbae3acc0"
    },
    {
      "type": "WEB",
      "url": "https://github.com/authlib/authlib/commit/891e3f10ae5989176211f8be35dcc7cd40aa92c6"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/authlib/authlib"
    },
    {
      "type": "WEB",
      "url": "https://github.com/authlib/authlib/releases/tag/v1.9.0"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:L/I:L/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Authlib: Unvalidated request_uri fetch in JWT-secured authorization request (JAR) leading to SSRF"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline

Sightings and recorded dates, grouped by day · UTC

JSON
Events included in the timeline

Loading timeline…


    Loading…

    Loading…

    Loading…

    Related by attack behaviour

    Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.