Skip to content
Security

Three days from launch to 4.75 million exposed records

Moltbook exposed 4.75 million records days after launch: one Supabase key in the bundle, no row-level security behind it. The check takes ten minutes.

7 min readKato Chojiro

Moltbook opened to the public on 28 January 2026. Within five days, researchers at Wiz had found a Supabase publishable key sitting in one of the site's JavaScript chunks, and behind that key, a production database with no row-level security on it at all. Not a weak policy. None. The key granted unauthenticated read and write access to everything: 1.5 million agent API tokens, 35,000 owner email addresses, another 29,631 from the early-access list, and 4,060 private conversations, some carrying plaintext OpenAI keys. Roughly 4.75 million records.

The founder had said publicly that he did not write a line of code. That detail got the attention and it is the least useful part of the story. The key being in the bundle was not the bug. That key is meant to be in the bundle. The bug was that nothing stood behind it.

The key is supposed to be public

A Supabase publishable key is a signed token whose only meaningful claim is a Postgres role. Every request carrying it arrives as anon. The key is an identity, not a permission. Two things decide what that identity can do: the table grants, and row-level security. Builder-generated code assumes both are configured. Neither is, by default, in the direction that matters.

So there is a failure mode that looks like nothing at all. The app works. The dashboard reports a healthy project. The key in the bundle is the key that belongs in the bundle. And every row of every table is one request away, because the layer that was going to refuse that request was never switched on.

Ten minutes, from outside, against your own app

This is the check Wiz ran, in the order they ran it. It works on any deployed site, whatever built it. First, pull the scripts the page loads and search them for a Supabase project and key.

bash
# 1. Every script the deployed page loads.
curl -s https://yourapp.example \
  | grep -oE 'src="[^"]+\.js"' | cut -d'"' -f2 | sort -u

# 2. Each of those, searched for a Supabase project and key.
curl -s https://yourapp.example/assets/index-abc123.js \
  | grep -oE 'https://[a-z0-9]+\.supabase\.co|sb_publishable_[A-Za-z0-9_-]+' \
  | sort -u

If that prints a project URL and a key, you have found exactly what an attacker finds, and nothing is wrong yet. The second command is the one that tells you whether it matters. Ask the REST API how many rows a table holds, and ask it to return none of them.

bash
curl -s -o /dev/null -D - \
  "https://<project>.supabase.co/rest/v1/profiles?select=*&limit=0" \
  -H "apikey: <publishable key>" \
  -H "Prefer: count=exact"

Read the Content-Range header that comes back. */0 is clean: the anonymous role can see nothing, which is what row-level security is for. A 401, or a 42501 permission error, is also clean, and means the grant never reached anon in the first place. A number is the finding, and the size of the number is the size of the incident. */40312 means an unauthenticated request just counted forty thousand rows it had no business counting.

Run it against the tables that would hurt, in that order: anything holding email addresses, payment identifiers, message bodies or role columns. A clean profiles table tells you very little if orders was never covered.

The fix, in an order that does not take the app down

  1. In a development project, turn RLS on for one table you can afford to break and watch what stops working. Every read that disappears had no policy behind it. This is the cheapest map of your own authorization model you will ever get.
  2. Write that table's policies to authenticated, with a separate with check on update so nobody can reassign a row to someone else while editing one they legitimately own.
  3. Move anything that genuinely needs to bypass policies into server-side code holding the secret key. If the browser needed the service role to work, the design was wrong before RLS was the problem.
  4. Repeat per table, ordered by what publishing that table would cost you.
  5. Only then enable RLS in production, one table at a time, watching error rates between each.

The asymmetry worth remembering

Wiz contacted Moltbook's maintainer at 21:48 UTC on 31 January. The first fix was live at 23:29 that evening and the last one just after 01:00. Under four hours, once somebody knew. The exposure had been running since launch.

That asymmetry is the whole argument for checking rather than worrying. Closing this costs hours. Finding out whether you need to costs ten minutes. What sits between them is nobody having run the command, and nobody runs it because the app looks fine from the inside, which it will keep doing right up until it appears in somebody else's write-up.

Every engagement that opens with this finding ends in the same place, so we wrote up what Supabase row-level security on an AI-built app actually has to cover, table by table. Moltbook is not an outlier in anything except the size of the number attached to it.

KC

Written by Kato Chojiro

Takes AI-built MVPs to production: security, data, and the infrastructure underneath.

Connect on LinkedIn
Book a Free Call

Want a second pair of eyes on your app?

The Production Check is free, and the written report is yours to keep.

Book a Free Call