← Back to blog
Open Response Type Vulnerability (2024): How To Protect Your OAuth Login

Open Response Type Vulnerability (2024): How To Protect Your OAuth Login

authors photo
Written by Dante Lex
Thursday, August 29th 2024

Do you use OAuth for your app login? If that’s the case, you need to be aware of a new vulnerability discovered on July 29.

What’s the Open Response Type vulnerability

The Open Response Type vulnerability compromises the security of OAuth, a popular authorization protocol that allows users to log in to websites using credentials from another Identity and Access Management (IAM) service like Google, using the response_type header.

When you log into a site using a service like Google or Facebook, the website requests a special code that is then used to verify your identity. This code is usually transmitted securely, but the Open Response Type vulnerability allows an attacker to trick the website into obtaining this code via web address (URL).

Impact for affected websites

The danger happens if the website also has an XSS vulnerability that allows attackers to inject malicious JavaScript into the web page to read the URL, including the secret code used for creating access tokens.

The consequences of this vulnerability are severe: once an attacker obtains this code, they can use it to impersonate the user and gain unauthorized access to their account. This access can persist even after the XSS vulnerability is patched, as the attacker can create new sessions that bypass standard protections like HttpOnly session cookies.

XSS makes it to the OWASP’s Top 10 every year, so it’s important to take steps to protect your users!

Reproduction steps

As previously mentioned, OAuth is a protocol that allows a website to access resources on another service on behalf of the user.

  1. When logging in, the user is first redirected to the service provider (like Github) to grant permission via an authorization URL.

server.js

app.get('/login/github', async ctx => {
    const state = generateState();
	const url = await github().createAuthorizationURL(state);
	
	setCookie(ctx, "github_oauth_state", state, {
		path: "/",
		secure: true,
		httpOnly: true,
		maxAge: 60 * 10,
		sameSite: "Lax"
	});

	let res = url.toString() // e.g https://github.com/login/oauth/authorize?response_type=code&client_id=XXX&state=YYY

	return ctx.redirect(res);
})
  1. Once permission is granted by the user, GitHub redirects the user back to the original website, including a special authorization code in the URL:

server.js

app.get('/login/github/callback', async ctx => {
	const code = ctx.req.query("code")?.toString() ?? null;
	const state = ctx.req.query("state")?.toString() ?? null;
	
	const storedState = getCookie(ctx).github_oauth_state ?? null;
	
	if (!code || !state || !storedState || state !== storedState) {
		throw new Error("Invalid state");
	}
	
	// ...
})
  1. This code is then exchanged for an access token that allows the website to access the user’s information securely.

server.js

app.get('/login/github/callback', async ctx => {
	// ...

	try {
		const tokens = await github().validateAuthorizationCode(code);
		
		// use tokens.accessToken to access the user's information
		
		const session = await auth().createSession(EXISTING_USER_ID, {});

		setCookie(ctx, "session", session.id, {
			path: "/",
			secure: true,
			httpOnly: true,
			maxAge: 60 * 10,
			sameSite: "Strict"
		});

		return ctx.json({ok: true})
	} catch (e) {
		throw new Error(e.message)
	}
})

An attacker can exploit the Open Response Type vulnerability by modifying the authorization URL’s response_type query parameter.

Instead of the standard code value, the attacker changes it to code,token causing the secret OAuth code to be included in the URL’s hash fragment (https://onboardbase/login/github/callback#code=123), which many applications do not handle correctly, instead of a query parameter (https://onboardbase/login/github/callback?code=123).

Note that the original exploit used Google’s OAuth service, but while doing tests ourselves, we found our login provider Github wasn’t vulnerable to this attack―the callback URL is always correctly set, undependently of response_type.

If the application fails to process the code from the fragment and leaves it in the URL, the attacker has a reliable way to extract it via XSS attack and use it to request an access token.

5 mitigation tactics

To mitigate this vulnerability, websites need to adopt more secure practices when handling the OAuth login process.

1. Subdomain redirection

One effective strategy against the Open Response Type vulnerability is to process OAuth codes on a separate origin, like a subdomain, to leverage cross-origin rules preventing XSS on one domain from accessing the URL of another, thereby thwarting the attack.

So for example, if you have an API subdomain handling the OAuth code, the attacker’s XSS code won’t be able to access it from the main domain hosting your front-end because there is no HTML element to create XSS vulnerabilities from:

onboardbase.com/login

<a href="https://api.onboardbase.com/login/github">Log in with Github</a>

api.onboardbase.com

app.get('/login/github', async ctx => {	
	// ...
})

app.get('/login/github/callback', async ctx => {
	// ...
})

Because it’s a subdomain and not an entirely different domain, the user’s session can be maintained across both domains via cookies.

If you use a monolithic architecture, consider using a subdomain to integrate OAuth. With a simple serverless function hosted on AWS, Vercel, or Cloudflare, for example.

2. httpOnly cookies

If for some reason our OAuth code is exposed and the social login is compromised, you can still protect your users on your own service by using httpOnly cookies to prevent JavaScript from accessing it.

This way, even if an attacker manages to steal the OAuth code, they won’t be able to use it to access the user’s account on your platform right away. They can still access the user’s account on the social login service, so it’s only a matter of time before they can access your service too, but if the leak is caught early, there is still a possibility to invalidate all sessions.

// create cookie
setCookie(ctx, "session", session.id, {
    path: "/",
    secure: true,
    httpOnly: true,
    maxAge: 60 * 10,
    sameSite: "Strict"
});

// make sure the cookie is httpOnly on the server
const sessionCookie = getCookie(ctx, 'session')

if (!sessionCookie || !sessionCookie.httpOnly) {
    throw new Error("Invalid session cookie")
}

You can even sign the cookie to prevent tampering, like a JSON Web Token.

3. Error redirect

Another mitigation strategy involves eliminating unexpected fragments from the URL by redirection to make sure the secret OAuth code is never exposed.

Upon receiving an incorrect request https://onboardbase/login/callback#code=123, the endpoint should redirect the user to a different, static URL https://onboardbase/login?error=invalid_state to return an error. This way, a hacker cannot access your secret code.

app.get('/login/callback', async ctx => {
    const code = ctx.req.query("code")?.toString() ?? null;
    const state = ctx.req.query("state")?.toString() ?? null;

    const storedState = getCookie(ctx).github_oauth_state ?? null;

    if (!code || !state || !storedState || state !== storedState) {
        return ctx.redirect('https://onboardbase.com/login?error=invalid_state')
    }

    // ...
})

4. Input validation

This vulnerability is only possible if the website has an existing XSS vulnerability in the first place, so it can’t hurt to check twice how you process user input.

You should always sanitize and validate user inputs by ensuring that they do not contain any executable scripts or malicious code before processing:

  • Filter out or escape HTML tags and special characters, both on the client and server side.
  • Test expected types and lengths (seems trivial but it causes vulnerabilities all the time e.g Heartbleed 2014, Crowdstrike 2024 …). A library like Zod helps a lot.
  • Use a Content Security Policy (CSP) to prevent inline scripts from running.

Most new frameworks like Next.js, Vue.js, Astro.js, etc. have built-in protections against XSS, but it’s always good to double-check with a data validation library like Zod at runtime:

import { z } from "zod";

const myFormSchema = z.object({
    email: z.string().email(),
    password: z.string().min(8),
});

const form = myFormSchema.parse(DUBIOUS_USER_FORM_INPUT); // throws an error if the input is invalid

5. Rotate your OAuth secrets

If your website is compromised, all you can do is apply the suggested patches and invalidate all sessions and force users to log in again.

It’s also a good idea to rotate your OAuth secrets beforehand as a precautionary measure.

If you’re not doing so already, it’s important to have a secret management solution like Onboardbase in place to store and rotate your OAuth secrets securely:

  1. Sign up, create a new project and deployment environment
  2. Add your OAuth secrets
  3. Add the Onboardbase CLI to your CI/CD pipeline npm install -g onboardbase
  4. Then log in with onboardbase login
  5. Pick your newly created environment with onboardbase setup
  6. Finally use the onboardbase run command to inject your OAuth secrets at runtime as environment variables.
  7. No need to change your code

Check the installation guide to quickly get started using your regular CI/CD pipeline or programming language.

Conclusion

The Open Response Type vulnerability poses a significant threat to web applications with existing XSS vulnerabilities, but it only takes a few steps to protect your users:

  • Use subdomain redirection to process OAuth codes
  • Use httpOnly cookies to protect your users
  • Redirect to an error page if the URL is incorrect
  • Validate all inputs
  • Rotate your OAuth secrets

Also make sure to subscribe to Onboardbase to securely manage your OAuth secrets! It only takes a few minutes to set up.

Subscribe to our newsletter

The latest news, articles, features and resources of Onboardbase, sent to your inbox weekly