Skip to main content

W3W

Initializing...

Web3 Security9 min readPublished 2026-08-09

What is Row Level Security in Supabase, and why did its absence cause the Moltbook breach?

Row Level Security (RLS) is PostgreSQL's per-row access control that makes Supabase's public anon API key safe to ship in client-side JavaScript. When RLS was never enabled, that key behaved as a database admin credential — which is how Wiz obtained 1.5 million API tokens and 35,000 email addresses from Moltbook in February 2026.

What is Row Level Security in Supabase?

Row Level Security is a native PostgreSQL feature (`ALTER TABLE ... ENABLE ROW LEVEL SECURITY`) that restricts which rows a query can read, insert, update, or delete based on a per-row policy expression. Supabase exposes your Postgres database to browser clients through PostgREST, and your project's public (anon) key is designed to be shipped in the front-end bundle. That design only holds together if RLS policies are the actual gatekeeper between the anon key and your data.

When RLS is enabled on a table with no policies, even the table owner gets denied-all by default. You opt in to access with explicit policies. When RLS is disabled on a table, the anon role can read and modify every row in it over the public REST API.

Why did the Moltbook breach happen?

Moltbook, an AI-agent social network, deployed a Supabase backend and shipped the project's anon key in client-side JavaScript, expecting RLS to protect the tables — but the tables had no enforced RLS-level access control. Wiz researchers retrieved the database over the public REST API without authentication and obtained, per their own report: 1.5 million API authentication tokens, 35,000 email addresses, and 4,060 private direct messages between agents. They confirmed write access as well, modifying live posts until the maintainer applied read and then write restrictions across three fixes over the span of roughly two hours on January 31–February 1, 2026.

The failure was not that a key was in the bundle — that's by design. The failure was that Row Level Security was entirely absent behind it, giving an otherwise-public identifier full read/write authority. This is the same class of misconfiguration documented in CVE-2025-48757 (published May 29, 2025, CVSS 9.3), where 303 Supabase endpoints across 170+ Lovable-generated sites had zero or insufficient RLS policies.

How do I check if my Supabase project is exposed?

Open the SQL Editor in your Supabase dashboard and run this query to see every table's RLS state:

RLS audit query
SELECT c.relname, c.relrowsecurity
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = 'public'
AND c.relkind = 'r'
ORDER BY c.relname;

`relrowsecurity = false` is an exposed table; a fully configured table returns `true`. Then test the REST path the way an attacker does: open the browser's network tab, grab the anon key, and call `https://<project-ref>.supabase.co/rest/v1/<table>?select=*` with no Authorization header beyond the anon key. If data comes back, RLS is not enforced on that table. A scanner can flag the anon key as `present` and stop; this manual call is what confirms whether a policy is actually protecting the table.

How do I enable Row Level Security after an AI tool generated my app?

  1. 1Enable RLS on every public-schema table. In Supabase, the Table Editor enables RLS by default only for tables you create through the dashboard. Tables created by an AI coding tool via SQL look like ordinary tables — they ship RLS off. Apply per table: ALTER TABLE "table name" ENABLE ROW LEVEL SECURITY;
  2. 2Add per-row policies that reference the authenticated user. Use auth.uid() when the row has an owner column, and grant a public read policy only to tables that are genuinely public data. A blanket "grant public read on user data" policy is the same level as having no policy.
  3. 3Separate sensitive columns from client-accessible ones. RLS operates on rows, not columns. If a row mixes a public field (username) with a secret (an API token), an RLS row-policy grants access to both even when it should be individual-level. Move stored credentials to a server-only table or Supabase Edge Functions that return derived values, never raw keys.
  4. 4Re-test authenticated vs. anon. Once policies exist, issue both anon and authenticated requests to the same REST endpoint and verify each returns exactly the visible rows.

Is enabling RLS enough for a production app?

No. RLS is the baseline that makes an anon key safe to expose, but it is not an app firewall: browser-facing secrets belong in a server layer, not in the client bundle, and third-party dependencies must stay current across the deployment. The practice that holds is to treat RLS as defense in depth — least privilege, narrow policies, and server-side authentication for anything above plain row access.

Where does this actually apply?

  • Supabase anon key: safe to expose client-side only when RLS is complete. Otherwise it behaves like a master credential.
  • auth.uid(): the standard policy expression that restricts rows to the logged-in user.
  • RLS vs. credentials: RLS is row-based database-level access; it does not stop secrets shipped in a client bundle. Put secrets on the server.
  • Tables from AI code: always audit. Some tools create tables with RLS off, others on; verify each table individually.
  • The database boundary: every request reaches the database through the anon key and is filtered by row policy. That is why a single missing policy becomes an unauthenticated full read-write rather than a partial leak.

Quick answers

Is it safe to expose a Supabase anon key in client-side JavaScript?

Only if Row Level Security is enabled for the table to protect. Without RLS, the anon key grants unauthenticated read/write access to that data — the failure behind Moltbook's February 2026 breach.

How do I check whether RLS is enabled in Supabase?

Query pg_class for relrowsecurity on each public table (documented above). relrowsecurity = false on any table is a risk. Also test the REST endpoint with select=* as an anonymous user.

Does RLS prevent all server-side data leaks?

No. RLS is row-level database access control. It doesn't stop client-side secrets in bundles, and it's insufficient if a table mixes sensitive and non-sensitive columns in one row. Secrets belong in environment variables and server-side functions.

This is the exact failure class the Vibe-Code Hardening Audit catches on a live project — and it ships the fix as a remediation PR rather than a report.