An unauthenticated open redirect in Authlib's OpenIDImplicitGrant and OpenIDHybridGrant authorization endpoint lets a remote attacker cause the authorization server to issue an HTTP 302 to an attacker-chosen URL by submitting an authorization request that omits the openid scope.
OpenIDImplicitGrant.validate_authorization_request in authlib/oidc/core/grants/implicit.py:
def validate_authorization_request(self):
if not is_openid_scope(self.request.payload.scope):
raise InvalidScopeError(
"Missing 'openid' scope",
redirect_uri=self.request.payload.redirect_uri, # ← raw, unvalidated
redirect_fragment=True,
)
redirect_uri = super().validate_authorization_request()
...
OpenIDHybridGrant.validate_authorization_request in authlib/oidc/core/grants/hybrid.py shares the same pattern.
Both methods perform the openid scope presence check before delegating to super().validate_authorization_request(), which is where AuthorizationEndpointMixin.validate_authorization_redirect_uri validates the requested redirect_uri against the client's check_redirect_uri(...). The InvalidScopeError thrown by the scope check therefore carries attacker-controlled self.request.payload.redirect_uri.
OAuth2Error.__call__ in authlib/oauth2/base.py renders any error with a non-empty redirect_uri as an HTTP 302:
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)
A malformed authorization request that selects OpenIDImplicitGrant or OpenIDHybridGrant and omits the openid scope is therefore redirected to a fully attacker-chosen URL.
This is a variant of the issue fixed in commit 3be08468 ("fix: redirecting to unvalidated redirect_uri on UnsupportedResponseTypeError") that was missed in the OIDC Implicit and Hybrid grants.
OpenIDImplicitGrant or OpenIDHybridGrant (standard OIDC Implicit or Hybrid flow support).response_type that matches either grant: id_token, id_token token, code id_token, code token, or code id_token token.scope does not contain openid.redirect_uri value.No user authentication, no consent, no valid session, no CSRF token, and — notably — no valid client_id are required. The scope check runs before any client lookup, so any client_id value (including nonexistent ones) reaches the vulnerable code path.
The following unauthenticated GET is sufficient to induce the authorization server to redirect a victim's browser to an attacker-controlled URL:
GET /oauth/authorize
?response_type=id_token
&client_id=anything
&scope=profile
&redirect_uri=https%3A%2F%2Fevil.example.com%2Fphish
&state=s&nonce=n HTTP/1.1
Host: victim-op.example
Server response:
HTTP/1.1 302 Found
Location: https://evil.example.com/phish#error=invalid_scope&error_description=Missing+%27openid%27+scope&state=s
redirect_uri that has not been validated against the client's registered URIs, even in error responses. The state parameter is echoed back, giving the attacker site a stable correlator.Any application using Authlib as an OIDC provider that registers OpenIDImplicitGrant and/or OpenIDHybridGrant — i.e. anyone supporting the Implicit flow or the Hybrid flow (response_type=code id_token, etc.) — is affected. Clients of an Authlib-based OP are not directly affected; this is a server-side issue.
Authorization servers that only register the plain AuthorizationCodeGrant (code flow, with or without PKCE and the OpenIDCode extension) are not affected by this specific variant: the code-flow grant validates redirect_uri before raising scope errors. If you were affected by the sibling issue fixed in 3be08468 (UnsupportedResponseTypeError), you should already be on 1.6.10 or later; this advisory is independent of that fix.
The attached fix-oidc-open-redirect.patch reorders each method to delegate to its super (or call validate_code_authorization_request for Hybrid) first, and then performs the openid-scope check with the validated redirect_uri variable.
# authlib/oidc/core/grants/implicit.py
def validate_authorization_request(self):
redirect_uri = super().validate_authorization_request() # runs client + redirect_uri validation
if not is_openid_scope(self.request.payload.scope):
raise InvalidScopeError(
"Missing 'openid' scope",
redirect_uri=redirect_uri, # validated
redirect_fragment=True,
)
try:
validate_nonce(self.request, self.exists_nonce, required=True)
except OAuth2Error as error:
error.redirect_uri = redirect_uri
error.redirect_fragment = True
raise error
return redirect_uri
An equivalent transform is applied to OpenIDHybridGrant.validate_authorization_request, invoking validate_code_authorization_request first and only then checking is_openid_scope.
Alternatively, inline a client = query_client(request.payload.client_id) + client.check_redirect_uri(request.payload.redirect_uri) guard before populating redirect_uri on the error — the pattern used in 3be08468.
The patch also adds regression tests analogous to test_unsupported_response_type_does_not_redirect from commit 3be08468, asserting rv.status_code == 400 and rv.headers.get("Location") is None for an unregistered redirect_uri with a non-openid scope.
No clean server-side workaround exists short of patching. Partial mitigations:
OpenIDImplicitGrant and OpenIDHybridGrant if the Implicit and Hybrid flows are not required. (RFC 9700 deprecates the Implicit flow and discourages Hybrid flows, so this is recommended anyway.)/authorize endpoint with a reverse proxy rule that rejects requests containing both a redirect_uri parameter and a scope that does not include openid when response_type matches the vulnerable set. This is fragile and not recommended as a primary control.3be08468 — prior fix for the same class of issue in UnsupportedResponseTypeError (Authlib 1.6.10)5d2e603e):
OpenIDImplicitGrant.validate_authorization_request — authlib/oidc/core/grants/implicit.pyOpenIDHybridGrant.validate_authorization_request — authlib/oidc/core/grants/hybrid.pyOAuth2Error.__call__ — authlib/oauth2/base.py (renders errors with redirect_uri as HTTP 302)AuthorizationEndpointMixin.validate_authorization_redirect_uri — authlib/oauth2/rfc6749/grants/base.py (the validation that is bypassed){
"severity": "MODERATE",
"github_reviewed_at": "2026-05-13T01:36:03Z",
"cwe_ids": [
"CWE-601",
"CWE-863"
],
"nvd_published_at": null,
"github_reviewed": true
}