September 21, 2025
How misconfigured Supabase APIs exposed sensitive data across thousands of organizations
Khaled Hassan
eq
, neq
, gt
, ilike
) enable data dumps.Supabase is an open-source backend-as-a-service built on top of PostgreSQL. It was created to help developers build applications quickly without losing the flexibility and reliability of a traditional relational database. At its core, Supabase provides a managed PostgreSQL instance, but it extends far beyond that by automatically generating REST and GraphQL APIs directly from the database schema. This means that as soon as you create or modify a table, the data becomes accessible through queryable endpoints.
In addition to its database and APIs, Supabase includes authentication, storage, realtime subscriptions, and serverless functions all designed to integrate seamlessly with PostgreSQL.
The philosophy of Supabase is to offer the speed and convenience of platforms like Firebase while maintaining the power, transparency, and standards of an SQL-based ecosystem. This combination has made it a popular choice among startups and developers who want to move fast while retaining full ownership and control of their data.
While conducting a penetration test on a client’s application, one particular header appeared in almost every HTTP request:
X-Client-Info: supabase-js-web/2.43.4
And this was the first good indicator that the target application database was built using Supabase, a backend-as-a-service platform that offers managed PostgreSQL databases on the cloud and this observation immediately shifted my testing mindset, since Supabase applications share a very specific architecture, where the REST API is exposed directly from the database schema.
As I dug deeper, I noticed that all the API endpoints followed a predictable pattern. The structure of requests was highly consistent, with the same rest/v1 path and SQL-like query parameters.
For example:
GET /rest/v1/users?select= HTTP/2
GET /rest/v1/keys HTTP/2
GET /rest/v1/teams HTTP/2
GET /rest/v1/organizations HTTP/2
After recognizing the pattern, I cross-referenced it with Supabase’s own documentation: Supabase REST API. Supabase advertises its auto-generated API as:
Supabase automatically generates a RESTful API from your database schema using PostgREST. This lets you interact with your database directly from the browser or alongside your own API server.
The API is instantly reflected from your schema, so any database updates are immediately available. Documentation is auto-generated in the dashboard.
It provides:
At first glance, this feels magical.
My reaction was literally: “Wait… does this mean I can expose my database tables as a REST API with almost zero configuration? Oh really? Wow.”
After reading further into the documentation of supabase, I came across something called the select
parameter.
This parameter instantly caught my attention because it works exactly like the SELECT
clause in SQL. It defines which columns and related tables you want to return in a request.
Examples:
select=*
→ return all columns.select=id,name,email
→ return only the id
, name
, and email
columns.select=id,teams(id,name)
→ return the user id
plus related teams
data.So in practice:
GET /rest/v1/users?select=*
is the REST API equivalent of:
SELECT * FROM users;
Coo, let’s try something.
I tried it directly on the endpoint:
GET /rest/v1/Users?select=* HTTP/2
PoC1
The response? An error. At first I thought maybe the dataset was too large to be returned in one shot. I then added a limit:
GET /rest/v1/Users?select=*&limit=100
And suddenly boom! The first 100 user records of private users information has returned.
I repeated the same approach with different endpoints. For example, the projects endpoint:
GET /rest/v1/Projects?select=*&status=eq.active HTTP/2
And this returned a full list of all active projects on the platform, whether they were intended to be private or public.
It became clear that once the Supabase API was exposed without strict row-level security (RLS) rules.
This demonstrated a critical point: if endpoints are exposed without proper per-row access controls, it is possible to retrieve the entire database tables via the API. In other words, without the appropriate protections in place, the REST surface can be used to dump table contents that should not be public.
While experimenting, I also discovered that filters themselves could be abused. Parameters like eq
, neq
, gt
, and ilike
are designed to make querying flexible, but when authorization checks are missing, they become powerful exploitation tools.
For example:
GET /rest/v1/users?select=id,email&status=neq.disabled
or
GET /rest/v1/users?select=id,email&created_at=gt.2025-01-01
Supabase exposes almost the full range of PostgreSQL operators through the REST API. Filters are powerful, and when authorization is missing they become effective exfiltration tools.
Examples of usable filters:
eq
equal, neq
not equal, gt
greater than, gte
greater or equal, lt
less than, lte
less or equallike
, ilike
, match
, imatch
in
, is
, isdistinct
cs
, cd
, ov
, sl
, sr
fts
, plfts
, phfts
, wfts
or
, and
, not
Examples of abuse:
GET /rest/v1/users?select=id,email&status=neq.disabled GET /rest/v1/users?select=id,email&email=ilike.%@gmail.com GET /rest/v1/users?select=id,email&id=gt.1
With no per-row restrictions, these requests behave exactly like running SQL in psql.
While investigating, I obtained a dataset of companies using Supabase. What I found was alarming. Hundreds, possibly thousands, of organizations appeared to use Supabase in ways that left them exposed to this class of issue. The list included small projects, notable startups, medium sized businesses, and some large enterprises that used Supabase for MVPs or internal tools.
I compiled a list of affected parties and reported the issue through responsible disclosure channels. Many organizations acknowledged the report and patched the issue.
After reading further, I discovered that Supabase has something called RLS (Row Level Security). This is not a Supabase invention, it’s a built-in PostgreSQL feature but Supabase relies heavily on it to control what data an API client can see.
Row Level Security allows you to define rules that apply per row in a table. Instead of only granting or denying access at the table level, you can say “a user can read rows where user_id = auth.uid()” or “a user can only update rows they created.”
Row Level Security allows you to define rules that apply per row in a table. Instead of only granting or denying access at the table level, you can say “a user can read rows where user_id = auth.uid()” or “a user can only update rows they created.”
Example with the users table
In the case of a users table, you might enable RLS and then add a policy like this:
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Individuals can view their own profile"
ON public.users
FOR SELECT
USING (id = auth.uid());
Here’s what this does:
ENABLE ROW LEVEL SECURITY
turns RLS on for the table.USING (id = auth.uid())
part enforces that a query only returns rows where the row’s id
matches the ID of the currently authenticated user.auth.uid()
is a Supabase helper function that extracts the user’s ID from their JWT.GET /rest/v1/users?select=*
returns every user in the database, potentially including sensitive fields like emails, metadata, or identity provider IDs.
id=gt.1
or id=neq.1234
, the policy ensures that only their own row is visible.GET /rest/v1/users?select=*
The filters themselves (eq
, neq
, in
, gt
, ilike
, etc.) aren’t vulnerabilities, they’re powerful query operators. The real risk comes from APIs being exposed without RLS policies. In that situation, attackers can abuse filters to enumerate all records in the users
table.
For example:
GET /rest/v1/users?select=id,email&status=neq.disabled
→ dumps all active users.GET /rest/v1/users?select=id,email&email=ilike.%@gmail.com
→ enumerates every Gmail address.GET /rest/v1/users?select=id,email&id=gt.1
→ pulls every user after the first row.Without row-level controls, the database responds exactly as if you had run these queries inside psql.
/rest/v1/<table>
).users
but forgets to enable it on related tables like projects
or teams
, those other endpoints remain exposed.public
schema.Here’s a concise, safe checklist you can use to tell if a target is using Supabase. I focus on passive / non-destructive signals you can observe from the browser.
X-Client-Info
response/request header that contains supabase
or supabase-js
(for example: X-Client-Info: supabase-js-web/2.43.4
).supabase.co
(for example: abcd1234.supabase.co
)./rest/v1/<table>
or /rest/v1/
in network traffic.realtime
or realtime.supabase.co
.@supabase/supabase-js
or referencing createClient
/ supabase.createClient
in page source or bundled JS.package.json
or yarn.lock
containing @supabase/supabase-js
or postgrest-js
.supabaseClient
, supabase.auth
, supabase.from(...)
.Thanks for your reading!
Stay secure with DeepStrike penetration testing services. Reach out for a quote or customized technical proposal today
Contact Us