Why you still need a backend

This post is meant to be a write-up for a 0-day against a super new product as well as an understanding of why backends have and will continue to exist and replacing them with smarter frontend solutions cannot mitigate the inherent security risks that come along with it.

First, let's talk about the exploit and the product, it went something like this:

Introducing http://onthedl.app - instant password protection for your react applications. You can add it as a component to any React app in just 5 lines of code

The 5 lines of code look similar to this:

import Protected from "react-passphrase"

// Wrap Protected around your App component
<Protected projectId="9fe190c9-5514-4f1f-b0f0-daddf7265d7">
  <App />
</Protected>

At the time of executing the 0-day, the tweet was 4 hours old, and it had a decent amount of traction, about 100 likes and 25 retweets. But we noticed that paid subscriptions were also available at $2.99 which used Stripe in live mode, which meant that the exploit now also has a real-world value attached to it.

Even on the surface, I felt that there was no way this is possible, because there is no way for a frontend component to effectively store a password securely. The way the product seemed to combat this was to have an API that compares the password in a database corresponding to the project ID in the protected component and returns matched: true or matched: false depending on whether the password matched.

The good things: it didn't seem to be overtly vulnerable to XSS (obviously) or SSTI (less obviously), so that definitely meant that the input was being sanitized somewhat.

The bad thing: the data is quite obviously accessible in the build whether under password protection or not. At the very least, this should be inserted similar to how SSR is carried out. Even assuming that the project ID and protected content is effectively obfuscated, the ID is immediately accessible in local storage (essentially the React state) upon a successful login, which can then be immediately picked up if the underlying site is vulnerable to XSS. Our guess is that all of these flaws are intentional, however it should be made absolutely clear to prevent new users from falling into the understanding that this is in any way secure.

Example key-value local storage

The more interesting thing: the endpoint seems to be vulnerable to a timing attack which has some interesting conclusions for us to make:

  • Passwords are not hashed but rather compared in plaintext (more on this later)
  • This often means a direct string equality comparison or a lexical comparison
Payload not matching the length of the actual password
Payload matching the length of the actual password

The important understanding here is that effectively translates into a possibility for sniffing the password by attempting to get close to another string that is close to it, and this is even more effective if your set password is a common password.

Why you need a backend, pt. 2

This is where we come to the most egregious part of the exploit, and the downsides of performing user access control entirely in the frontend (as well as a couple of other security issues).

When you login to the site, it uses magic.link to sign you up or in. This approach has been becoming more and more common in regards to authentication but it signals to a potential attacker that this website might have chosen to not implement authentication in the traditional way of using a backend. Stateless authentication is something the internet has wrangled with for a long time, but that's something to get into another time.

So then the question becomes, how egregious is it? Quite a lot. Let's see what the means:

We can see that we are requesting three columns, id,name,passphrase in a particular order and for the given user_id. Note that this is an XHR request, so this means it was sent in the frontend, how do we achieve that?

Therein lies a big flaw and why frontends utterly suck at managing credentials, you can see the apikey and authorization headers in plain view, which means anyone can simply get them in any request by opening up their browser console, and ofcourse, this also means the keys are already leaked in the build. If the client is on HTTP, they're also suspectible to MITM attacks since the secrets would be transmitted in plaintext. Even if the component aspect of it is innovative gatekeeping, it is all too easy to leak other users' data, especially more dangerous if the other users do not have a full understanding of the underlying security being provided. This is something that is typically something mitigated using a backend, however it seems to be an implementation on top of specific Supabase auth policies where secrets have attached context, but as people unfamiliar with it, we will not comment further on it.

Using the privileged secrets, we can then modify the query slightly to suit our exploit:

This is enough for us to leak all the project IDs and passphrases that currently exist. Since the passphrases are also stored in plaintext, we assume for the purpose of being shown in the UI, which is what a potential user would be paying for, we eliminate any assumption of security that existed.

Next, we thought of expanding the scope of attack, we noticed that the host for all these queries was a Supabase subdomain (which implies a hosted project) and a quick search confirmed that one of their official methods of using their DB is using PostgREST, the API patterns exhibited here seem to match their specs exactly. We got to work trying to figure to find out what else there is, by default the app only queries two tables, projects and subscriptions. PostgREST simplifies this for us, by eliminating the table name from the query, we hit the root endpoint, which provides us all the existing tables.

We did a bit more searching to see if there's anything else exposed and that seemed to be it.

Then we had another question, "are these keys read-only?" It is a quite common way of differentiating security levels, maybe these keys are only being used in the frontend because they are read-only. We decided to carry out a small-scale white-hat exploit that did not impinge on the underlying product, and change all passphrases to hacked. It was intended as a dual intent of conveying to existing users that their information was exposed in an exploit (at the time, only 11 users, so very low-impact). When we received the updated payload, we confirmed our suspicions that indeed these keys had write permissions.

My new password being shown in the UI

Ending conclusions

It should probably be noted that this product is a side project developed by one person (we think?), but I think there's an underlying message about how reinventing existing ideas often has several pitfalls. We don't intend for this white-hat exploit as a discouragement from building new things but in the age of newer, innovative and often more commonplace, 0-day exploits, keeping the security of your application airtight is paramount. We are still cybersecurity enthusiasts and we have a lot to keep learning.

But more apparent is the need for backends to continue existing, the only solution to not leak secrets is to never have them, and if you have them, you must build it with the safest solution possible, not something that seems to work.


Acknowledgements

This exploit was executed in tandem with Kiteretsu, a cybersecurity enthusiast and personal friend of mine. Most of the SQLi (if it's fair to call it that) was their research first and secondly, a challenge for me.

We also took a redacted database dump as proof of the exploit.