This post is the second in a series describing OAuth implementation issues that put companies at risk. We create these posts to share rich technical details, drawn from real-world use cases, to educate the broader industry on the nature of these errors, their potential impact, and how to avoid them to better protect API ecosystems.
This post details issues identified in Expo, a popular framework used by many online services to implement OAuth (as well as other functionality). The vulnerability in the expo-auth-session library warranted a CVE assignment – CVE-2023-28131. Expo created a hotfix within the day that automatically provided mitigation, but Expo recommends that customers update their deployment to deprecate this service to fully remove the risk (see the Expo security advisory on the topic).
The security gaps Salt Labs identified made services using this framework susceptible to credentials leakage, allowing:
Again, to fully mitigate these risks, Expo recommends that customers update their Expo deployment.
OAuth (Open Authorization) is a modern, open authorization standard designed to allow cross-application access delegation – for example, allowing your application to read data from your Facebook profile. Combined with the proper extensions, OAuth can also be used for authentication – for example, to log into your application using Google credentials.
Our first blog post in this series presented how we could have exploited several OAuth vulnerabilities in Booking.com (a company with $16 billion in annual revenue) to take over accounts. In the Booking.com example, the company directly implemented the OAuth functionality as part of its site’s design and development life-cycle. Other online services may choose a different path, and take advantage of off-the-shelf services that provide OAuth functionality, to reduce their development overhead.
The main purpose of the Expo framework is to develop mobile applications. It allows developers to build high-quality native apps for iOS, Android, and web platforms using a single codebase. It provides a set of tools, libraries, and services that simplifies the development process. One of the included services is OAuth, which lets developers easily integrate a social sign-in component into their website.
One of the most important motivations for using an “off-the-shelf” framework such as Expo is to provide better security controls. We found that indeed, Expo is highly secure, however Expo is a big framework supporting many features other than just OAuth, making it hard to secure every part of the framework.
Evidently, when we inspected the OAuth front, it seems the door was left open just enough for us to be able to spot critical issues that have in turn affected the implementation of hundreds of Expo users.
The following explanation is relevant to almost every website, and not specific for Expo implementation. If you already read our blog on Booking.com, or are familiar with OAuth, you can skip that explanation and go directly to the “The Expo framework” section below.
Assume you are John, and you want to connect to Randomsite.com using your Facebook account.
What happens when you click on “Login with Facebook”?
After John clicks on login with Facebook, randomsite.com opens a new window to the following address:
In this address, the client_id tells Facebook that the App is randomsite.com.
If it’s the first time that John connects to randomsite.com, Facebook will ask John if he agrees to give randomsite.com permission to access his Facebook account (only to read his email address for authentication).
If it’s not the first time he’s connecting, this Facebook access will happen automatically.
Note the redirect_uri parameter – it tells Facebook where to send the token in Step 4-5.
Facebook prepares a secret token for randomsite.com and redirects the browser back to redirect_uri. The exact redirection:
randomsite.com reads the token from the URL and uses it to talk directly with Facebook using the following API:
The response is email@example.com.
The flow in the example is called “implicit grant type,” which is common in single-page applications and native desktop applications that don't have a back end.
OAuth also uses an “explicit grant type,” which is similar to “implicit grant type” but the server (randomsite.com) receives a code instead of a token and needs to make an additional request to Facebook to exchange it to a token. This approach is more secure, but we’re using the implicit grant type example here because it is easier to understand.
Google, Apple, and other well-known vendors follow similar flows (across both implicit and explicit grant type). A newer method takes advantage of the PostMessage feature instead of a redirection, but we’re not addressing that use case in this post. Using redirection is still the most common approach.
The Expo framework for mobile app development is used by 650,000 developers and big companies.
Before discussing challenges at real websites, let’s build our own simple application to see how it looks from the developer perspective.
First, we need to create a new app in Facebook (developers.facebook.com), and according to the Expo documentation, we also need to add a custom redirect_uri - https://auth.expo.io/@account_name/project_name in the Facebook configuration of our app. After adding those configurations, Facebook will agree to send the secret token to Expo, like it was our own website.
Yes, we just configured Facebook to send the secret token to a domain we don’t control – what can possibly go wrong?
Note we used Facebook for the example, but it applies to other vendors as well - Apple, Google, Twitter etc.
The second step is to write the actual application using the Expo framework. We can just copy-and-paste code from the official Expo documentation:
(Understanding the code is not important for the post)
Let’s see how it works – in other words, let’s look at what requests the mobile app is making:
It may seem like too much information, but if you look closely - Steps 2-5 are what you already saw in the beginning of the blog with the randomsite.com example.
In Steps 1 and 6, expo.io acts as a wrapper to the OAuth flow.
Let’s explain it step by step:
In OAuth, the goal of the attacker is to steal the token or code of the victim. My general methodology in OAuth research is to cause unexpected behaviors of the flow by changing every parameter I can, to see how these manipulations advance me toward the ability to launch a successful attack.
In the case of Expo, I immediately noticed that in Step 6, Expo.io sends the token to a value that was provided in Step 1. What will happen if the attacker changes that value to his domain?
Reminder - this is the link from Step1:
This page reads the query parameter “returnUrl” and sets the cookie accordingly.
Let’s change the returnUrl to hTTps://attacker.com (https is not allowed, so I tried to insert capitals letters and it worked), which sets the RU (Return Url)in the cookie to https://attacker.com.
We, as the attacker, send this link to the victim.
What the attacker expects:
Dan clicks on the malicious link (can be from either the website or mobile app - it doesn’t matter).
The link starts a login flow against https://auth.expo.io, which will redirect Dan to Facebook, which will redirect Dan back to https://auth.expo.io with a secret token.
At the last step of the flow, https://auth.expo.io will send the secret token to https://attacker.com
A potential snag … before Expo redirect Dan to Facebook, it shows him a confirmation message that he must approves:
While it is possible to trick Dan (the victim) to click on the message, we don’t consider this approach to be a smooth attack, and therefore we want to bypass this message and perform the attack without user interaction.
The most common trick is clickjacking. However, since Facebook is involved in the flow, it’s not possible to use iframe.
In Step 2, https://auth.expo.io automatically sets the cookie RU to https://attacker.com, before the confirmation message appears.
No matter what Dan choses, the RU is already set.
What will happen if a hacker sends a second link to Facebook?
When Dan clicks on the first link:
Expo.io will set the cookie RU to https://attacker.com
When Dan clicks on the second link:
Facebook will redirect Dan back to Expo.io with a token, and then Expo.io sends the token to the value RU - https://attacker.com
The only problem? This flow is not feasible. Dan (the victim) will not click on two links.
And upload the exploit to my domain: https://example-domain.com
The exploit does two simple things:
Using the exploit, we can steal a token from a victim. The next part is to use it to log into the victim’s account. We will demonstrate this account takeover using the Codecademy site.
Codecademy is a popular online platform that offers free coding classes across a dozen programming languages. Companies including Google, LinkedIn, Amazon, Spotfiy, and others use the site to help train employees, and the site boasts ~100 million users. It’s also a big customer of Expo.
The Codecademy OAuth flow looks similar to our local application:
The only difference is that Expo.io uses a different path - https://auth.expo.io/@codecademy/codecademy-go
Let’s change the exploit accordingly:
If the victim clicks on our link with the exploit, his token will automatically be passed to our domain.
To complete the account takeover, we start a regular login flow, but we replace the token with the victim token.
Codecademy.com is not alone. The landing page of Expo features lots of household brand names, companies with millions of users each.
To be clear – sites and applications using Expo are not necessarily vulnerable – the implementations need to use the AuthSession Proxy of Expo, which is part of the social-in component.
Instead of checking and reverse engineering every customer individually, we decided to obtain a list of customers that use https://auth.expo.io (AuthSession Proxy).
Expo has social media platforms like a forum and a Discord server, where customers can ask questions and post information about their applications.
We searched for the value “auth.expo.io/@”, and found 34 vulnerable companies.
We verified that the vulnerability exists in some of those targets – again, real companies with active websites and users.
We wanted to find even more customers, so we did a search on auth.expo.io/@ in GitHub.
Some of the results lead to real applications that you can download on Google Play. Those are small applications so we decided to not mention their names in the blog.
Expo was a pleasure to work with in resolving this vulnerability.
Expo no longer sets the RU cookie without end user approval. With this mitigation, the exploit no longer works without the victim's approval.
The company also updated the confirmation message to be clearer.
Less than two weeks after our disclosure, Expo deprecated this service (Expo AuthSession Redirect Proxy - auth.expo.io). The company’s documentation now tells developers not to use it.
Expo communicated all these discoveries and changes to its customers in the following blog post:
Security vulnerabilities can happen in any website – it’s the response that matters.
Expo responded very quickly. The team released a first fix after only a few hours! We were very impressed by the commitment to security and willingness to take swift action to protect its customers. Well done, Expo!
Now it’s up to all users of Expo to ensure they’re no longer using this deprecated service.
We worked through the following timeline in this coordinated disclosure process. Again, we thank Expo for taking action so quickly to resolve these critical vulnerabilities.
The unsafe consumption of APIs can lead to security breaches, exposing sensitive data, user credentials, or proprietary information, as attackers may exploit vulnerabilities in API usage to gain unauthorized access, execute arbitrary code, or perform unauthorized actions within the system.
Improper Inventory Management is the ninth security threat listed in the OWASP API Security Top 10. By exploiting this vulnerability, attackers can gain unauthorized access to sensitive data, or even gain full server access through old, unpatched or vulnerable versions of APIs.