October 28, 2026
Four Redirects, One URL, Full Account Takeover: An OAuth Chain Autopsy
Not one sophisticated bug. Three string comparisons that were each almost right.

By Raj Namdev
8 min read
Uber paid $7,500 for the cleanest version of this chain in 2017. The first report in the class is from 2014, and the newest version of it shipped seven days ago. Every version dies at the same place: a string comparison that was clever instead of exact.
In April 2014, a researcher named melvin reported that a Meteor-built app called Respondly had an open redirect in its Twitter OAuth flow โ "obtaining the OAuth token and verifier of the victim," as the report puts it. A month later, the OAuth working group published Security Advisory 2014.1 and gave the pattern a name: Covert Redirect.
Twelve years later, in April 2026, LY Corporation paid $1,000 for an open redirect on page.line.me that sent an OAuth authorization code to an attacker-controlled endpoint. Same class. Same year the class turned twelve.
The chain never died, and I want to show you why โ hop by hop, through the reports that got paid for it.
โ ๏ธ Verification note: HackerOne's report pages loaded this session but returned truncated content โ I could confirm titles, reporter handles, and the technical mechanism for several reports, but not the specific severity ratings, bounty amounts, and upvote counts. Those are flagged inline where I couldn't re-confirm them directly.
What the chain needs from you
OAuth hands credentials to the browser and trusts a URL to carry them home. The entire security model rests on one comparison: does the redirect_uri in the request match the redirect_uri the client registered?
That's the load-bearing wall. An open redirect by itself is a shrug โ most programs pay $50 to $500 for one. But an open redirect inside a trusted origin is a transport primitive: it lets the attacker pass the validator (because the check sees the legitimate domain) and land the token anywhere else. OWASP's own page on open redirects calls chains with OAuth the canonical example, and names the two defenses that stop it: exact redirect_uri matching, and PKCE.
Every report below is a case where one of those wasn't there.
The cleanest chain ever paid for
HackerOne report #202781 โ Uber, filed by @ngalog โ โ reporter and chain mechanism confirmed directly this session. The report's own description walks through it:
- The misconfigured validator. Uber's Facebook OAuth application accepted any
redirect_urimatching the formathttps://auth.uber.com/login?*. Not an exact match โ a shape. Anything after the ? passed. - The parameter that extends the path. Inside that allowed shape, @ngalog found the
next_urlparameter โ attacker-controlled, and it redirected. - The second redirect.
https://login.uber.com/logoutredirected based on theRefererheader โ another transport primitive, wearing a different parameter. - The landing. Referer-based redirect to the attacker's site, with the token attached.
Four redirects, one URL for the victim to click. Uber's triage called it "an awesome single-URL PoC" that "would have allowed for a full account takeover and was a very creative and cool bug."
โ ๏ธ $7,500 and 427 upvotes are as reported in your sources (the TOP100 upvoted list); I wasn't able to re-confirm these two figures directly from the report page itself this session, since the page content was truncated before reaching bounty/vote data.
Notice what made it work: not one sophisticated bug. Three string comparisons that were each almost right โ a wildcard match, an unvalidated parameter, a Referer trust โ chained into account takeover.
Where the validator broke, across a decade
The validation gap is the class, and it has an unbroken record from 2018 to this year:
- Bohemia #405100 (2018): the endpoint "is checking if the Redirect_uri is starting with
https://xbox.dayz.comand not checking the ending bits, as such its possible to inject anything after that." OAuth tokens exfiltrated. A prefix match is not an exact match. - GSA #665651 (2019): same flaw on
login.fr.cloud.gov/oauth/authorizeโ "stealing users oauth tokens through redirect_uri parameter." $750, High โ per your source's citation of an analyst comment in the report thread; not independently re-confirmed by me this session. - Lichess #3099816 (2025): a community site's OAuth flow accepted a
redirect_uripointing off-domain entirely, after the user had already authenticated at the real lichess.com. - LY Corporation #3423013 (2025): the open redirect on page.line.me "was identified because redirect destinations were not properly restricted to trusted domains" โ inside an OAuth 2.0 authorization flow, "the authorization response to be sent to an attacker-controlled endpoint," exposing the code for exchange into a valid access token. $1,000 per your source's citation of an LY staff comment; not independently re-confirmed this session.
And the parameter can hurt twice. Uber #397497 โ stored XSS on the authorize endpoint itself, via the redirect_uri parameter โ the validation surface turned injection surface, leading to account takeover. $3,000, per your earlier series entry; I wasn't able to re-fetch the full JSON for it again this session.
The fix, every time, is the same sentence: compare the whole string. When OWASP says "exact matching," it means exact.
The 2026 model: the chain without an open redirect
Here's what twelve years of patching did. It didn't kill the chain โ it forced the chain to get interesting.
The best report of the modern era: Reddit #1567186, filed by fransrosen in 2022 โ โ reporter and the first chain step confirmed directly this session (remaining steps truncated on the page, but match your description). Rated critical, $10,000, 509 upvotes per your source. The mechanism is a masterpiece:
- The attacker generates a
stateparameter from their own Apple sign-in flow with Reddit. - The victim gets a link that loads
www.redditmedia.comโ Reddit's intentional JS sandbox โ in an iframe. The sandbox useswindow.nameto pass the parent window's URL, fragment included, to the scripts inside. - Inside the sandbox, the sign-in link for Apple is rebuilt with two edits: the attacker's state, and a response-type switch โ
response_typechanged fromcodetocode+id_token,response_modeforced tofragment. - Reddit expected tokens to arrive via
response_mode=web_messageโ apostMessage. "Reddit is not expecting to get any sensitive tokens in the URL fragment." But the other response modes were never disabled. - Victim clicks once. The main window's URL becomes
https://reddit.com/#state=โฆ&code=โฆ&access_token=โฆโ and the sandboxed script, same-origin with the iframe, reads it throughwindow.name.
One click, and the attacker signs in as the victim. Frans's own mitigation notes are the whole lesson in two lines: strip the fragment before sending the URL to the sandbox, and restrict the redirect_uri to a page that can't run attacker JavaScript.
The same "other modes weren't disabled" gap showed up at the library level this June: CVE-2026โ41479 in Authlib โ โ
fully confirmed this session. An unsupported response_type turns the authorization endpoint into an unauthenticated open redirect, because UnsupportedResponseTypeError's error-path redirect fires before client lookup and before any redirect_uri validation. One request, a 302 to anywhere, no client registration, no user, no state. CVSS 5.4 (Medium) โ not in your original draft but worth including. The fix quote from the advisory is a monument to how this class keeps happening: per RFC 6749, "only redirect with the error if the client exists and the redirect_uri has been validated against it."
And seven days before this was drafted, the Model Context Protocol's official TypeScript SDK shipped a batch of advisories covering related territory โ โ all three confirmed directly this session, and the technical descriptions match closely:
CVE Issue Severity CVE-2026โ111769 HTTP client transports and OAuth requests followed cross-origin redirects, re-sending custom headers and (on 307/308) request bodies โ including refresh_token and client_secret Medium, CVSS 6.5 CVE-2026-111771 Server-side bearer auth accepted tokens issued for a different service, because audience wasn't checked Medium, CVSS 6.8 CVE-2026-111767 Experimental tasks weren't tied to the session that created them โ any client sharing a task store could read others' tasks High, CVSS 8.6
(One correction: CVE-2026โ111769 is specifically about re-sent headers/bodies on cross-origin redirects, not a redirect-credential bug in exactly the same shape as the OAuth reports above โ it's a related-but-distinct class, which I've kept distinguishable in the table rather than folding into the same "redirect carries token" framing.)
The oldest chain in this piece is twelve years old. The newest is eight days.
The origin check that wasn't
One more report, because it's the purest example of the whole thesis: Twitter/X #129873, filed by filedescriptor in 2016 โ โ
reporter, program, and general mechanism confirmed directly this session (the .search() regex detail itself was truncated on the page, but matches technical write-ups of this well-known bug elsewhere).
Digits โ Twitter's phone-auth SDK โ validated the origin of postMessage events with this line:
-1 !== this.config.get("sdk_host").search(t.origin)-1 !== this.config.get("sdk_host").search(t.origin)String.prototype.search() takes a regex. The origin string gets coerced. And in a regex, a dot matches any character. So the attacker's www.d.gits.co matched the trusted www.digits.com โ every dot a wildcard, the "origin check" a pattern match. The forged Digits iframe silently bound the attacker's phone number to the victim's account; password reset via Digits finished the takeover. No user interaction, no awareness.
619 upvotes, $5,040 per your source โ not independently re-confirmed by me this session.
The fix filedescriptor recommended is a favorite sentence in twelve years of this class: "a simple string comparison is enough." Use ===. Use indexOf. Nothing clever.
That's the autopsy finding, and it repeats in every report above: the wildcard match on Uber's Facebook app, the starts-with match at Bohemia, the fragment that wasn't stripped at Reddit, the regex origin at Twitter, the before-validation redirect in Authlib. Not one of these chains beat a correct comparison. They all beat a clever one.
What the class pays
โ ๏ธ Every bounty figure in this table comes from your original sources (the TOP100 upvoted dataset and in-thread comments); I could not independently re-confirm the dollar amounts against the live report pages this session due to content truncation.
Chain reached Report Paid Redirect only Lichess #3099816, 2025 open redirect standing alone Token exposure, code intercepted GSA #665651, 2019 $750, High Code โ access token compromise LY #3423013, 2026 $1,000 ATO via injection surface Uber #397497, 2018 $3,000 Silent ATO via forged origin Twitter #129873, 2016 $5,040 Full ATO, four-redirect chain Uber #202781, 2017 $7,500 One-click ATO, response-type switch Reddit #1567186, 2022 $10,000
A redirect that goes nowhere pays triple digits. A chain that ends with the attacker inside the account pays four or five. The distance between the two is always the same distance: how many of the four requirements โ validation, transport, landing, state โ you can connect in one URL.
Where to look first
From the decade of receipts, the highest-yield tests, in order:
- The exact-match test. Encode, append, and fragment the registered
redirect_uriโ //, @, /..,%2e%2e, a #, a second ?. Any byte that survives validation and changes the landing page is the whole chain, hop one. - The shape-matching config. Uber's app trusted
login?*. Look for the wildcard in the OAuth application config, not just the endpoint โ the app registration is where the pattern match lives. - The mode switch. If the client uses
web_messageor a popup flow, try forcingresponse_mode=fragmentorquery. The client only defends the mode it expects โ Authlib's 2026 CVE is this at the library level, and fransrosen's Reddit chain is this at the application level. - The origin check in the SDK. Grep every
postMessagehandler for.search(,.match(,includes(on an origin. If it's not ===, it's the Digits bug waiting for its era. - The state. If you can replay a flow with a state you generated and the server completes it, the account-takeover version of the chain is live.
The autopsy result
The 2014 advisory named Covert Redirect assuming the industry would fix a named, documented class. Instead the class got a twelve-year resume: $750, $1,000, $3,000, $5,040, $7,500, $10,000 โ one chain per year, on average, across old monoliths, SaaS, government clouds, and now the MCP ecosystem. The comparison bug never got smarter. The software around it did.
Next in this series: the attack that needs no redirect at all โ winning a race condition with one packet.
Sources
- Uber #202781 โ reporter, program, and three-step chain mechanism confirmed directly this session; bounty ($7,500) and upvotes (427) are from your original TOP100-list sourcing, not re-confirmed.
- Reddit #1567186 โ reporter and first chain step confirmed directly this session; severity, bounty, and upvotes from your original sourcing, not re-confirmed.
- Twitter/X #129873 โ reporter, program, and general mechanism confirmed directly this session; the
.search()detail, bounty, and upvotes from your original sourcing, not re-confirmed. - CVE-2026โ41479 โ Authlib (SentinelOne) โ fetched and fully confirmed this session, including the exact mechanism and RFC 6749 fix quote. Note: the advisory itself gives two slightly different publish dates (June 22 vs June 26, 2026) in different places.
- CVE-2026โ111767, 111769, 111771 โ MCP TypeScript SDK advisories (GitLab Advisory Database) โ all three fetched and confirmed this session, October 20, 2026 publish date.
- Bohemia #405100, GSA #665651, Lichess #3099816, LY Corporation #3423013, Uber #397497, Respondly #7900 โ not independently re-fetched this session; carried over from your original sourcing.
- OWASP โ Open Redirect
- OAuth Security Advisory 2014.1 โ Covert Redirect