Skip to main content

Command Palette

Search for a command to run...

Story of Abusing a Fully Secured redirect_uri in an OAuth Flow

Published
8 min read
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:

  1. The Application (e.g., YouTube) redirects you to the Provider (e.g., Google) with a set of parameters including client_id, redirect_uri, scope, and state

  2. You authenticate with the Provider

  3. The Provider sends you back to the Application's redirect_uri with an authorization code (or token)

  4. 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:

  1. 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_uri

  2. Dispatcher 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 state parameter, postMessage abuse, or Unicode tricks in window.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/login

  • No dispatcher endpoint. No JavaScript involved. The redirect was a clean 301 HTTP redirect

  • No state parameter 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.com

  • Path: /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: https

  • Host: a.com

  • Fragment: @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:

  1. The server conducts a single round of URL decoding before validating the redirect_uri, which is standard behavior

  2. before redirection, a function caused to perform an additional round of decoding

  3. Double-encoded characters (%2523 for #) 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 :)