Notes · Dissecting Real Systems
growing
One Defect, Three Surfaces
CORS allowlists, OAuth redirect URIs and open redirects have separate vulnerability literatures and the same bug — a structured identifier tested with a string operation. The fix is one sentence, and a standards body finally wrote it down.
The complexity of implementing and managing pattern matching correctly obviously causes security issues.
— Torsten Lodderstedt, John Bradley, Andrey Labunets, Daniel Fett, Best Current Practice for OAuth 2.0 Security (RFC 9700, 2025) §4.1.3
Cite this
Mangalapilly, Y. J. (2026, August). One Defect, Three Surfaces. Saṃhitā Notes. https://yesudeep.com/blog/one-defect-three-surfaces/ @online{mangalapilly2026one,
author = {Yesudeep Jose Mangalapilly},
title = {One Defect, Three Surfaces},
journal = {Sa\d{m}hit\=a Notes},
year = {2026},
month = {August},
url = {https://yesudeep.com/blog/one-defect-three-surfaces/},
urldate = {2026-08-12},
} Yesudeep Jose Mangalapilly. “One Defect, Three Surfaces.” Saṃhitā Notes, 2026. https://yesudeep.com/blog/one-defect-three-surfaces/. TY - ELEC
AU - Mangalapilly, Yesudeep Jose
TI - One Defect, Three Surfaces
T2 - Saṃhitā Notes
PY - 2026
UR - https://yesudeep.com/blog/one-defect-three-surfaces/
Y2 - 2026-08-12
ER - One structural flaw across three security surfaces. CORS origin allowlists, OAuth redirect_uri validation and open redirects fail the same way: pattern matching stands in for exact string equality. By the end you'll know why those three fail identically, what changed between RFC 6749 and RFC 9700 and why the earlier text invited the bug, why an open redirector anywhere on your domain is an OAuth vulnerability, and why a token that names its own verification algorithm asks the wrong party to decide.
Three findings from earlier pieces in this series, put side by side.
From the CORS piece: a site restricting cross-origin reads to subdomains of advisor.com accepted definitelynotadvisor.com, because the check was a suffix test and definitelynotadvisor.com ends with advisor.com.
From the same research: a site trusting btc.net accepted https://btc.net.evil.net, because the check was a substring test.
And a third, which has its own literature entirely: an OAuth authorization server that matched registered redirect URIs by prefix sent an authorization code to a URL the client never registered.
A URL is not a string. It is a parsed structure that happens to have a string serialization, and every check that treats the serialization as the thing is one clever hostname away from being wrong.
What OAuth got right, eventually
The interesting part of this story is that OAuth has now fixed it in a standards document, and the before-and-after is instructive.
RFC 6749, the 2012 OAuth framework edited by Dick Hardt, said this about validating the redirect URI:
the authorization server MUST compare and match the value received against at least one of the registered redirection URIs (or URI components)
Read the parenthesis. Or URI components. The specification explicitly permitted matching a part of the URI — which is to say, it invited exactly the prefix-matching implementations that turned out to be exploitable. The requirement was normative and the loophole was in the same sentence.
Thirteen years later, RFC 9700 closes it. §2.1:
When comparing client redirection URIs against pre-registered URIs, authorization servers MUST utilize exact string matching except for port numbers in localhost redirection URIs of native apps.
And §4.1.3 explains the reasoning, which is the sentence this article is built around:
The complexity of implementing and managing pattern matching correctly obviously causes security issues. This document therefore advises simplifying the required logic and configuration by using exact redirection URI matching. This means the authorization server MUST ensure that the two URIs are equal; see Section 6.2.1 of [RFC3986], Simple String Comparison, for details.
Important
Note what the fix actually is. Not "parse more carefully" and not "write a better regex" — remove the pattern matching entirely and compare against an enumerated list. The remedy for a class of parsing bugs is to stop needing a parser at the decision point.
BCP — Best Current Practice, an IETF document series for operational guidance rather than protocol definition. RFC 9700 is BCP 240 and formally updates RFC 6749, so the exact-matching requirement is now part of what conforming to OAuth 2.0 means.
Thirteen years is a long interval, and it was not a quiet one. In A Comprehensive Formal Security Analysis of OAuth 2.0 (Fett, Küsters and Schmitz, CCS 2016) the authors built a formal model of the protocol covering all four grant types and reported that they "discovered four attacks which break the security of OAuth," adding that the vulnerabilities "can be exploited in practice and are present also in OpenID Connect."
That is the cost of the parenthesis. A specification sentence that permitted matching a component instead of a URI produced a decade in which implementations were individually reasonable and collectively unsound, ending with a formal analysis of the standard itself finding attacks that a careful reading of the prose had not.
The same sentence, on a different surface
RFC 9700 then applies the identical rule to a mechanism that has nothing to do with redirects — cross-window messaging, in §4.17.2 — requiring authorization servers to "utilize exact string matching" against pre-registered origins, and adding that "Wildcard origins like * in postMessage MUST NOT be used."
That is the same remedy for the same defect on a third surface, written by the same working group in the same document. When one specification independently prescribes exact matching for redirect URIs and for message origins, the pattern has stopped being a coincidence.
Why an open redirect is an authentication bug
Open redirects look like a nuisance: an attacker gets your domain to bounce a user somewhere unpleasant, which is a phishing aid rather than a breach. RFC 9700 disagrees, and says so twice.
Clients and authorization servers MUST NOT expose URLs that forward the user's browser to arbitrary URIs obtained from a query parameter (open redirectors)… Open redirectors can enable exfiltration of authorization codes and access tokens.
The mechanism is worth tracing, because it shows how two individually-defensible decisions combine into a failure.
Your OAuth client registers https://app.example/callback, and the authorization server enforces exact matching. Good. Elsewhere on https://app.example there is a /go?to= endpoint that redirects wherever it is told — added years ago for a marketing campaign, reviewed by nobody, and not part of the authentication system by anyone's reckoning.
The attacker sends the user through a normal authorization request with the registered redirect URI. The server checks it, finds it exact, and issues the code to https://app.example/callback. The callback runs. And somewhere in the flow — a state parameter carrying a return path, a post-login redirect — the application forwards to the attacker's URL, with the code still in the Referer or in the URL itself.
The redirect URI validation was perfect. The credential left anyway.
Exact matching on the redirect_uri constrains where the code is delivered, not where it ends up. An open redirector anywhere on the registered origin extends the boundary to the whole web.
RFC 9700 covers a subtler case too, §4.11.2, where the redirect URI is correctly registered by an attacker who used dynamic client registration — and notes that in that flow "the user agent will be redirected to the phishing site regardless of the action taken by the user." Registration is not authentication.
The /go endpoint predated the current team. It took a to parameter and redirected, and it existed because a campaign in 2019 needed trackable outbound links. It had no session, read no cookies, and touched no user data, which is why three security reviews had walked past it.
The auth review was thorough and found nothing, because the auth review looked at the auth code. Exact matching on the redirect URI: correct. State parameter: present and validated. Token storage: fine.
What nobody drew was the line from /go to the callback — two endpoints on one origin, neither of them wrong, and a path between them that carried an authorization code off the domain.
The token that names its own algorithm
There is a second, different structural mistake in this area, and it is worth the contrast because it is not a parsing bug — it is a trust-direction bug.
A JSON Web Token carries a header naming the algorithm used to sign it. The verifier reads that field to know how to verify. Stated plainly, the arrangement sounds reasonable and is the whole problem: the credential tells the verifier how to check the credential.
RFC 8725 — JSON Web Token Best Current Practices, by Yaron Sheffer, Dick Hardt and Michael B. Jones — documents where that leads:
- The algorithm can be changed to "none" by an attacker, and some libraries would trust this value and "validate" the JWT without checking any signature.
- An "RS256" (RSA, 2048 bit) parameter value can be changed into "HS256" (HMAC, SHA-256), and some libraries would try to validate the signature using HMAC-SHA256 and using the RSA public key as the HMAC shared secret.
The second is the elegant one. The RSA public key is, as its name says, public. Tell the verifier the token is HMAC-signed and it will use that public value as a shared secret — which the attacker also has. The signature verifies. Nothing was broken cryptographically; the verifier was persuaded to use the wrong primitive.
The remedy in §3.1 is the deny-by-default move the closing piece of this series argues for in general:
Libraries MUST enable the caller to specify a supported set of algorithms and MUST NOT use any other algorithms when performing cryptographic operations. The library MUST ensure that the "alg" or "enc" header specifies the same algorithm that is used for the cryptographic operation. Moreover, each key MUST be used with exactly one algorithm.
The application decides the algorithm; the token is checked against that decision rather than consulted for it. The field stops being an instruction and becomes an assertion to verify.
Note
RFC 8725 does not ban alg: none, which surprises people. §3.2 allows that when a JWT is "cryptographically protected end-to-end by a transport layer" there "may be no need to apply another layer," and unsecured JWTs "can be perfectly acceptable." What it requires is that libraries neither produce nor consume them "unless explicitly requested to do so by the caller." The defect was never the algorithm — it was who chose it.
Sessions, briefly and concretely
The cookies piece covered the container. Two numbers about what goes in it, from OWASP, because they are checkable rather than aspirational:
Session identifiers must have at least
64 bitsof entropy to prevent brute-force session guessing attacks.
and the qualifier that catches homegrown schemes:
if any part of the session ID is fixed or predictable, the effective entropy is reduced
A session ID built as a random value concatenated with a user ID, or with a timestamp, has the entropy of the random part alone — and a 128-bit-looking token can be a 32-bit token wearing a costume.
The other rule is the one that turns a design decision into a vulnerability if skipped:
The session ID must be renewed or regenerated by the web application after any privilege level change within the associated user session… The session ID regeneration is mandatory to prevent session fixation attacks, where an attacker sets the session ID on the victim user's web browser instead of gathering the victim's session ID.
Notice the inversion, which is what makes fixation easy to miss when you are thinking about theft: the attacker does not need to take your session. They need to give you theirs, before you authenticate. If the identifier survives the privilege change, a value they already know is now a logged-in session — and the sibling-subdomain problem from the cookies piece is a way to plant it.
The habit worth taking away
The three defect surfaces reduce to one question you can ask at a code review, and it generalizes past the three surfaces here.
Is this check parsing, or is it matching text?
If a value has structure — a URL, an origin, an email address, a file path, a version constraint — and the code decides something security-relevant about it with startsWith, endsWith, includes, or a regular expression, that is the defect in this article regardless of which of the three surfaces it appears on.
The remedies, in descending order of preference:
- Enumerate. Compare against a list of exact, complete values. Exact matching is what RFC 9700 requires and it is the only option with no parsing at the decision point.
- Parse, then compare fields. If enumeration is impractical, use a real URL parser and compare scheme, host and port as separate values — never the serialization.
- Never pattern-match a structured identifier. If you find yourself writing a regular expression against a hostname, enumerate complete values or parse the hostname and compare its fields.
Warning
Watch for the "we only allow our own domain" check specifically. url.startsWith("https://app.example") admits https://app.example.attacker.net. A leading-dot suffix check, host.endsWith(".example.com"), admits a host the attacker registers as evil.example.com.attacker.net only if the check is on the full URL rather than the parsed host — which is precisely why the parsed host matters.
Lessons
- One defect, three surfaces. CORS origins, OAuth redirect URIs and return-to URLs are structured identifiers compared with string operations.
- RFC 6749 invited the bug in its own sentence — "or URI components" permitted partial matching. RFC 9700 removes it: exact string matching, with one localhost-port exception.
- The fix is to stop pattern matching, not to pattern-match better.
- An open redirector on the registered origin defeats exact matching, because exactness constrains delivery and not destination.
- A JWT's
algfield asks the credential how to check the credential. Pin the algorithm in the application; verify against it. alg: noneis not banned — the requirement is explicit caller opt-in, which makes this a trust-direction problem rather than an algorithm problem.- 64 bits of entropy, and no fixed parts. A predictable component reduces the effective entropy to the random remainder.
- Regenerate on privilege change, or a session identifier the attacker planted survives your login.
Practice
References
- Torsten Lodderstedt, John Bradley, Andrey Labunets, Daniel Fett. “Best Current Practice for OAuth 2.0 Security.” RFC 9700 (BCP 240), 2025. — exact string matching as a MUST, the pattern-matching indictment, and open redirectors as a token-exfiltration path
- Dick Hardt (Ed.). “The OAuth 2.0 Authorization Framework.” RFC 6749, 2012. — the "or URI components" clause that permitted partial matching for thirteen years
- Yaron Sheffer, Dick Hardt, Michael B. Jones. “JSON Web Token Best Current Practices.” RFC 8725 (BCP 225), 2020. — the none and RS256-to-HS256 attacks, and the requirement that the caller pin the algorithm
- Michael B. Jones, John Bradley, Nat Sakimura. “JSON Web Token (JWT).” RFC 7519, 2015. — the token format, and the Unsecured JWT it defines
- OWASP. “Session Management Cheat Sheet.” OWASP Cheat Sheet Series. — 64 bits of entropy, the fixed-component caveat, and regeneration as mandatory
- Tim Berners-Lee, Roy Fielding, Larry Masinter. “Uniform Resource Identifier (URI): Generic Syntax.” RFC 3986, 2005. — Simple String Comparison, the comparison ladder RFC 9700 points at
- James Kettle. “Exploiting CORS Misconfigurations for Bitcoins and Bounties.” PortSwigger Research, 2016. — the suffix and substring allowlist failures this article generalizes
How to cite
Mangalapilly, Y. J. (2026, August). One Defect, Three Surfaces. Saṃhitā Notes. https://yesudeep.com/blog/one-defect-three-surfaces/ @online{mangalapilly2026one,
author = {Yesudeep Jose Mangalapilly},
title = {One Defect, Three Surfaces},
journal = {Sa\d{m}hit\=a Notes},
year = {2026},
month = {August},
url = {https://yesudeep.com/blog/one-defect-three-surfaces/},
urldate = {2026-08-12},
} Yesudeep Jose Mangalapilly. “One Defect, Three Surfaces.” Saṃhitā Notes, 2026. https://yesudeep.com/blog/one-defect-three-surfaces/. TY - ELEC
AU - Mangalapilly, Yesudeep Jose
TI - One Defect, Three Surfaces
T2 - Saṃhitā Notes
PY - 2026
UR - https://yesudeep.com/blog/one-defect-three-surfaces/
Y2 - 2026-08-12
ER - Webmentions
Annotations
Thank you — your note is held for review and will appear once approved.
Thank you — your note is published.
Please sign in below to leave a note.
