Back to reports

The Supabase RLS Crisis: How We Found 2,270 Resumes, Plaintext Passwords, and Payment Data Exposed to the Internet

We scanned 741 Supabase-powered applications in a single batch. Fifteen critical-severity findings. Twenty-four high-severity findings. Real applications, real people, real data.

PublishedMarch 30, 2026

Published: March 27, 2026 Author: Breakglass Intelligence Category: Vulnerability Research Tags: Supabase, RLS, Data Exposure, OSINT, Web Security


We scanned 741 Supabase-powered applications in a single batch. What we found should concern every developer who has shipped a Supabase project in the last three years.

Fifteen critical-severity findings. Twenty-four high-severity findings. One AI recruiting platform with its admin key baked directly into a public JavaScript bundle — granting anyone full, unrestricted access to 2,270 candidates' resumes, interview transcripts, LinkedIn profiles, and personal email addresses. A wedding venue CRM storing passwords in plaintext. A logistics platform leaking nearly 22,000 shipment records under active GDPR jurisdiction. An export services company with Razorpay payment IDs sitting wide open.

This is not a theoretical attack. These are real applications, real people, and real data — exposed right now to anyone who knows where to look.

Live stats from our ongoing scanner: As of this writing, we have identified 5,371 Supabase-powered applications, 637 exposed credentials, 61 critical-severity findings, and 8 service_role key leaks. See the live dashboard at supabased.breakglass.tech. This post details our methodology and the most critical findings from one scan batch.


How We Found 1,740 Targets

Supabase is the dominant open-source Firebase alternative. Every Supabase project gets a unique subdomain on supabase.co and a REST API powered by PostgREST. Developers embed two keys in their client code: an anon key for public access and, sometimes incorrectly, a service_role key for admin access.

Both keys are JWTs. Both are structured the same way. Both show up in JavaScript bundles. The difference is catastrophic.

Our automated scanner built a corpus of 1,740 Supabase-powered applications through four passive reconnaissance channels:

GitHub Secret Scanning. Developers routinely commit .env files, configuration objects, and hardcoded credentials directly to public repositories. Searching for the Supabase JWT structure (eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 followed by a payload containing "role": "service_role") returns hundreds of live hits. Many of these repositories belong to applications still running in production.

Wayback Machine Crawling. The Internet Archive has captured JavaScript bundles from millions of web applications over the years. JS bundles almost never contain secrets intentionally — but developers who embed environment variables at build time leave permanent snapshots of their credentials in CDN-cached and archive-captured assets.

Certificate Transparency Logs. Every TLS certificate issued for a public domain is logged in CT logs. Supabase project references follow a predictable format (<project-ref>.supabase.co). We enumerated recently-issued certificates to identify new deployments before they appeared in search engines.

urlscan.io. This public scanning service captures page source, network requests, and HTTP headers for submitted URLs. JavaScript assets loaded from Supabase CDN origins appear in scan results alongside the page they were loaded on, often exposing the full initialization call — keys included.

From 1,740 identified targets, we ran deep analysis against 741 in our first scan batch. That batch produced the findings described in this post.


What Is Supabase RLS and Why Does It Matter

Before getting into specific findings, it helps to understand how Supabase's security model is supposed to work.

Supabase sits on top of PostgreSQL. When you create a table, Supabase automatically generates a REST API endpoint for it at:

https://<project-ref>.supabase.co/rest/v1/<table-name>

This endpoint is publicly reachable. Anyone with your project URL can attempt to query it. The only thing standing between your database and the internet is Row Level Security (RLS) — a PostgreSQL feature that defines which rows a given database role can read, insert, update, or delete.

Enabling RLS on a table is a single SQL statement:

alter table candidates enable row level security;

But enabling RLS alone does nothing useful. Without at least one policy, RLS defaults to deny all — no rows are returned to anyone. You then write policies to define what each role can access:

-- Only the authenticated user can see their own profile
create policy "Users see their own data"
on profiles for select
to authenticated
using ( auth.uid() = user_id );

This is the intended workflow: enable RLS, then write policies that are as restrictive as possible.

The problem is that RLS is opt-in, not the default.

Tables created through the SQL Editor in Supabase do not have RLS enabled automatically. The developer has to remember to enable it. During a typical startup sprint — where speed matters and the database schema is changing daily — RLS often gets skipped entirely with the intention of "adding security later." For many projects, later never comes.


The Two Keys: A Critical Distinction

Every Supabase project ships with two API keys. Understanding the difference is the single most important thing a Supabase developer can know.

The Anon Key

The anon key is a JWT that carries the Postgres role anon. It is designed to be embedded in client-side code. It is safe to ship in your index.html, your JavaScript bundle, or your mobile app — as long as RLS is properly configured.

const supabase = createClient(
  'https://your-project.supabase.co',
  'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' // anon key — OK in client
);

When this key hits the PostgREST API, it runs queries as the anon role. If your RLS policies only grant anon access to public rows, the key is harmless in an attacker's hands. Queries return exactly what you intended public users to see and nothing more.

The service_role Key

The service_role key is a JWT that carries elevated Postgres privileges. It exists specifically to bypass RLS entirely. Every policy you have written — every access restriction you have configured — is ignored when a request arrives with this key.

This key is meant to run only on servers you control: background jobs, edge functions, admin dashboards behind authentication, migration scripts. It is never, under any circumstances, supposed to appear in client-side code.

When we decode a JWT and see this payload structure, we know we have found a critical vulnerability:

{
  "role": "service_role",
  "iss": "supabase",
  "iat": 1710000000,
  "exp": 1900000000
}

A service_role key embedded in a JavaScript bundle is not a misconfiguration. It is an open door.


Headline Finding: An AI Recruiting Startup — 2,270 Candidates Exposed

The most severe finding from this scan was an AI-powered recruiting platform.

The application's index.html loaded a JavaScript bundle that contained this initialization pattern:

const supabaseAdmin = createClient(
  'https://[redacted].supabase.co',
  'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.[redacted].[redacted]'
);

We decoded the JWT payload and confirmed the role claim was service_role. The key was active.

Using the standard PostgREST API with this key, we were able to enumerate every table in the public schema. The candidates table contained 2,270 records, each with:

  • Full name and personal email address
  • LinkedIn profile URL and scraped profile data
  • Uploaded resume files (stored in Supabase Storage, accessible via signed URLs that the service_role key could generate)
  • AI-generated interview transcripts
  • Hiring stage and internal recruiter notes
  • Phone numbers for candidates who provided them

Because the service_role key bypasses RLS, there were no restrictions on reads, writes, or deletes. An attacker with this key could have exfiltrated the entire candidate database, modified hiring decisions, or deleted all records permanently.

The data represents a significant privacy exposure for job seekers who submitted their information to a platform they expected to protect it.

Severity: CRITICAL CVSS-equivalent score: 9.8 Notification sent: March 25, 2026


Finding 2: A Wedding Venue CRM with Plaintext Passwords

A CRM application used by a wedding venue stored customer and staff credentials directly in its Supabase database — no hashing, no salting. The anon key was correctly used in this application, but RLS had never been enabled on the users table.

A simple query against the public endpoint returned this structure:

username: [redacted]
password: [redacted]
role: admin

username: [redacted]
password: [redacted]
role: coordinator

The inquiries table — also unprotected — contained 222 client records with full names, email addresses, phone numbers, event dates, and budget information. These are people who filled out a contact form on a wedding planning website, not people who signed up for a security-optional service.

Credential exposure in plaintext has cascading consequences. Password reuse is common. A single unprotected table becomes a credential stuffing source.

Severity: CRITICAL Notable: Plaintext password storage is a compounding violation — this application fails at both the database security layer and the application security layer simultaneously. Notification sent: March 25, 2026


Finding 3: A Logistics Platform — 21,979 Shipment Records Under GDPR

A logistics company operating in Europe was running a Supabase-backed shipment tracking and invoicing platform with RLS disabled on all tables. The exposure included:

  • 21,979 shipment records with origin/destination addresses, sender and recipient names, cargo descriptions, and tracking numbers
  • 492 invoices with billing details, company tax IDs, and transaction amounts
  • A gdpr_consents table containing the exact consent records the company was required to maintain under the General Data Protection Regulation — ironically, these consent records were themselves exposed without authorization

The GDPR angle here is particularly significant. This is not just a security finding — it is a regulatory one. Article 32 of GDPR requires organizations to implement "appropriate technical and organisational measures" to ensure data security. An unprotected PostgREST API endpoint is not an appropriate technical measure. EU supervisory authorities have issued fines for less.

Severity: HIGH Regulatory exposure: GDPR Article 32, potential Article 83 enforcement Notification sent: March 25, 2026


Finding 4: An Export Services Company with Razorpay Payment IDs

An export services company used Supabase to power its order management system. The orders table was accessible via the public PostgREST endpoint with no RLS policies in place.

The exposed records included Razorpay payment IDs and customer email addresses. Razorpay payment IDs are not secret by default — they appear in payment links and receipts — but combined with customer identity and order data, they enable targeted phishing attacks and facilitate refund fraud. An attacker who knows your Razorpay payment ID, order amount, and email address has enough to construct a convincing impersonation of Razorpay's support team.

Severity: HIGH Notification sent: March 26, 2026


How the Scanner Works

Our scanner follows a five-phase pipeline from discovery to confirmed data exposure.

Phase 1: Target Identification

We use the four passive sources described earlier (GitHub, Wayback Machine, CT logs, urlscan.io) to identify URLs and domains that load Supabase JavaScript clients. This phase is entirely passive — no requests are sent to target applications at this stage.

Phase 2: JS Bundle Extraction and Key Discovery

For each identified target, we fetch the page and follow the chain of script tags and JavaScript imports to the loaded bundles. We search each bundle for patterns matching Supabase client initialization:

createClient(<url>, <key>)

The <key> value is extracted and decoded as a JWT. We inspect the role claim in the payload:

  • anon — standard publishable key, lower risk
  • service_role — critical, immediate escalation

Phase 3: Project Reference Verification

The project reference is embedded in the Supabase URL and also appears in the JWT iss claim. We verify that the key is still active by making a single authenticated request to the PostgREST introspection endpoint:

GET /rest/v1/ HTTP/1.1
Host: [redacted].supabase.co
Authorization: Bearer [key]
apikey: [key]

A 200 OK response confirms the key is valid. A 401 indicates the project has been deleted or the key has been rotated.

Phase 4: Schema Enumeration

For verified live keys, we fetch the OpenAPI schema that PostgREST generates from the database. This returns every table and view in the public schema with their column names and types. No guessing required — Supabase publishes the schema to authenticated API clients automatically.

Phase 5: RLS Coverage Assessment

For each table, we attempt a SELECT query as the anon role (or as service_role if that is the key found). We check whether rows are returned and whether row counts match expected data volumes. Tables where anonymous queries return real data with no restrictions are flagged.

The full pipeline from target URL to confirmed finding takes approximately 4 seconds per target.


Why Developers Make This Mistake

This is not a failing of developers. It is a failing of defaults.

Supabase is designed to let you move fast. In development, you often want to query your tables without worrying about RLS. Supabase accommodates this by defaulting to RLS-disabled. The developer experience during the early stages of a project is frictionless because there are no access controls to configure.

The problem is that "frictionless in development" becomes "dangerously open in production" when developers ship without revisiting their security posture. Common contributing factors:

The "I'll add RLS later" trap. Early-stage projects skip RLS because the data is fake and the deadline is real. Later never comes before the first real users arrive.

Tables created via SQL Editor. The Supabase UI warns you when you create a table through its table editor and RLS is disabled. The SQL Editor does not enforce this warning. Developers who write migrations directly bypass the safety prompt.

Misunderstanding what the anon key does. Many developers believe that because the anon key has limited permissions, their data is protected. It is protected only if RLS policies are written and enabled. An anon key against a table with no RLS policies returns every row in the table.

Copying service_role into the wrong place. Admin panels, internal tools, and backend scripts often use the service_role key correctly. When those same developers build a public-facing frontend in the same codebase, they sometimes copy the initialization pattern — key and all — without recognizing that the context has fundamentally changed.


What Proper RLS Looks Like

Here is the pattern we expect to see on a candidates table in a recruiting application:

-- Step 1: Enable RLS (deny all by default)
alter table candidates enable row level security;

-- Step 2: Allow candidates to view only their own record
create policy "Candidates see their own record"
on candidates for select
to authenticated
using ( auth.uid() = auth_user_id );

-- Step 3: Allow recruiters with a specific role to see all candidates
create policy "Recruiters can view all candidates"
on candidates for select
to authenticated
using (
  exists (
    select 1 from staff
    where staff.auth_user_id = auth.uid()
    and staff.role = 'recruiter'
  )
);

-- Step 4: No one can modify records through the public API
-- (use service_role only from server-side admin functions)

Notice what is absent from this configuration: the service_role key anywhere near the client-side JavaScript.

Here is what we found instead in the affected recruiting application:

// In index.html — shipped to every visitor's browser
const supabase = createClient(
  'https://[redacted].supabase.co',
  'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' // service_role key
);

No RLS. Admin key in the browser. 2,270 people's data available to anyone who opened their browser's developer tools.


Responsible Disclosure Timeline

DateAction
March 21, 2026Scan batch initiated, findings identified
March 22, 2026Findings triaged, CRITICAL findings escalated for priority disclosure
March 25, 2026Direct notification sent to affected operators via email and abuse contact forms
March 26, 2026Remaining notifications sent, relevant data protection authorities notified for GDPR-jurisdictional findings
March 27, 2026Public disclosure of anonymized findings (applications identified by descriptor, not by name or domain)
30-day windowFull technical disclosure if no remediation confirmed

We do not publish project references, domain names, or identifying details of affected applications in this post. We have notified each operator directly. If you believe your application may be affected and have not heard from us, contact us at the address below.


Remediation Guidance

If you are a Supabase developer, run through this checklist now.

1. Audit your key types.

Open your Supabase dashboard. Go to Project Settings > API. You will see your anon (public) key and your service_role (secret) key. Search your entire codebase — including frontend build outputs — for the service_role key string. It should appear in exactly zero places that are accessible to a browser.

2. Audit RLS status for every table.

In the Supabase Table Editor, any table with RLS disabled shows a warning label. Take that warning seriously. For every table that is accessible through the public PostgREST API, RLS should be enabled.

Run this query against your database to get a full picture:

select
  schemaname,
  tablename,
  rowsecurity
from pg_tables
where schemaname = 'public'
order by tablename;

Any row where rowsecurity is false is a table with no access controls on the public API.

3. Test your own endpoints.

Using only your anon key, attempt to query each of your tables:

curl 'https://your-project.supabase.co/rest/v1/your_table?select=*' \
  -H 'apikey: YOUR_ANON_KEY' \
  -H 'Authorization: Bearer YOUR_ANON_KEY'

If you receive rows you did not intend to be public, your RLS configuration is incomplete.

4. Move admin operations server-side.

Any operation that legitimately requires service_role access — user management, bulk imports, background jobs, admin dashboards — should run in a Supabase Edge Function, a backend API route, or a server you control. The service_role key should live in an environment variable that never reaches the client.

5. Rotate any exposed keys immediately.

If you found your service_role key in a JavaScript bundle, client-side code, or a public repository, rotate it immediately in your Supabase project settings. Rotation invalidates the old key without requiring any schema or application changes. Do this before anything else.


The Broader Pattern

The 39 critical and high findings from this batch represent a systemic issue, not a collection of isolated developer errors. Supabase powers a large fraction of the startup and indie developer ecosystem. Its ease of use is a genuine competitive advantage. But ease of use creates a particular risk profile: developers who move fast through the tutorial, ship something that works, and defer the security configuration that the tutorial glosses over.

Supabase has made improvements — dashboard warnings for disabled RLS, email alerts for new exposed tables, better documentation. These are meaningful steps. But the defaults still leave too much to developer diligence at exactly the moment when developer attention is stretched thinnest.

The service_role key exposure is the more alarming finding. Every RLS misconfiguration is potentially recoverable — you enable RLS, write policies, and the data is protected. A service_role key in a JavaScript bundle that has been live for months may have already been discovered, copied, and used. There is no way to know whether your database was read by someone else before you fixed it. That uncertainty is its own consequence.


Contact

If you are an operator of an affected application and have not received our direct notification, or if you need help remediating a similar vulnerability, contact our consulting team at consulting.breakglass.tech.

If you are a security researcher who wants to collaborate on responsible disclosure for related findings, we are open to coordinated investigation.


Breakglass Intelligence conducts autonomous OSINT-driven security research. All findings in this report were obtained through passive reconnaissance and unauthenticated API queries against public endpoints. No systems were accessed beyond what is publicly reachable. All affected operators were notified prior to publication.


Sources:

Share