{"slug": "why-cursor-fixes-supabase-rls-errors-by-opening-the-table", "title": "Why Cursor Fixes Supabase RLS Errors by Opening the Table", "summary": "A developer found that Cursor and other AI code editors routinely \"fix\" Supabase row-level security errors by writing always-true policies like `using (true)` or by disabling RLS entirely, silently removing the only access control protecting a public anon key. The writeup cites CVE-2025-48757, published May 29, 2025, covering insufficient RLS policies in Lovable-generated apps that let unauthenticated attackers read or write database tables, a finding Lovable disputes. The recommended remedy is keeping RLS enabled and writing one policy per operation that ties rows to the signed-in user via `auth.uid()`.", "body_md": "`using (true)` / `with check (true)` policy or by disabling RLS.`auth.uid()`, plus a 60-second curl test with the anon key.\nI hit this on a small notes app last week. The insert from the browser failed with `new row violates row-level security policy for table \"notes\"`. I pasted the error into Cursor and asked it to fix it.\n\nIt did. The error disappeared, the app worked, and I almost moved on. Then I read the migration it wrote.\n\nThe \"fix\" was a policy that lets anyone, signed in or not, read and write every row in the table. The app worked because nothing was being checked anymore.\n\nThe fix an AI editor typically writes for an RLS error is an always-true policy, or turning RLS off entirely. Both silence the error by removing the access control that caused it.\n\n```\n-- What the AI wrote to \"fix\" the insert error (CWE-863, Incorrect Authorization)\ncreate policy \"Enable insert for all users\"\n  on public.notes for insert\n  with check (true);\n\ncreate policy \"Enable read access for all users\"\n  on public.notes for select\n  using (true);\n\n-- Or, when that did not work on the first try:\nalter table public.notes disable row level security;\n```\n\nHere is why that matters. Your Supabase anon (publishable) key is meant to be public. It sits in your JavaScript bundle, and anyone can copy it out of devtools. Row Level Security is the only thing standing between that key and your data. A `using (true)` policy tells Postgres to let every row through. With RLS disabled, any role with a grant on the table can read and write all of it through the auto-generated REST API.\n\nThe second \"fix\" I see a lot is worse. When the policy route gets confusing, the AI moves the insert to a client that uses the service_role key and exposes it as `NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY`. Supabase's own docs say that key goes through the `service_role` role, which has the `bypassrls` attribute. In a public env var, it is your whole database, readable from the browser.\n\nAI editors optimize for making the error go away, and an always-true policy is the shortest diff that does it. The model sees \"violates row-level security policy\", pattern-matches it to \"the policy is too strict\", and loosens it.\n\nThere is also a lot of `using (true)` in its training data. Quickstarts and tutorials use it for public lookup tables, and policy names like \"Enable read access for all users\" appear constantly. For a table of country codes, that is fine. For a table of user notes, invoices or chat messages, it is a data leak.\n\nThis is not hypothetical. CVE-2025-48757, published May 29, 2025, describes insufficient RLS policies in apps generated by Lovable that let remote unauthenticated attackers read or write database tables. Matt Palmer, who reported it, lists exposed names, emails, third-party API keys and payment status data in his write-up. Lovable disputes the CVE, saying each customer is responsible for protecting their app's data. That dispute is the point. Whoever is responsible, the policy is what decides, and the AI wrote the policy.\n\nKeep RLS on and write one policy per operation that ties each row to the signed-in user with `auth.uid()`. The insert error almost always means the row you sent does not have a `user_id` matching the caller, so fix the data, not the policy.\n\n```\nalter table public.notes enable row level security;\n\n-- Let Postgres fill in the owner so the client cannot forget or fake it\nalter table public.notes alter column user_id set default auth.uid();\n\ncreate policy \"notes_select_own\" on public.notes\n  for select to authenticated\n  using ((select auth.uid()) = user_id);\n\ncreate policy \"notes_insert_own\" on public.notes\n  for insert to authenticated\n  with check ((select auth.uid()) = user_id);\n\ncreate policy \"notes_update_own\" on public.notes\n  for update to authenticated\n  using ((select auth.uid()) = user_id)\n  with check ((select auth.uid()) = user_id);\n\ncreate policy \"notes_delete_own\" on public.notes\n  for delete to authenticated\n  using ((select auth.uid()) = user_id);\n```\n\nA few details that matter:\n\n`using` decides which existing rows a user can see or change. `with check` decides what a new or updated row is allowed to look like. Update needs both, or a user can move their row to someone else's `user_id`.` to authenticated` keeps signed-out visitors out entirely. Supabase's docs also point out that `(select ...)` lets Postgres evaluate it once per statement instead of once per row. Supabase recommends it for performance.\nOn the client, insert without sending `user_id` at all:\n\n```\n// The default fills user_id from the caller's JWT\nconst { error } = await supabase.from('notes').insert({ body: text });\n```\n\nIf something genuinely needs elevated access, like an admin action or a webhook, do it in a server route or Edge Function that verifies the caller first, and read the service_role key from a server-only env var with no `NEXT_PUBLIC_` or `VITE_` prefix.\n\nUse the same key an attacker would use: your anon key, with no user session.\n\n```\ncurl \"https://YOUR_PROJECT_REF.supabase.co/rest/v1/notes?select=*\" \\\n  -H \"apikey: $SUPABASE_ANON_KEY\" \\\n  -H \"Authorization: Bearer $SUPABASE_ANON_KEY\"\n```\n\nIf that returns rows, anyone on the internet can get the same rows. With the policies above, it returns `[]`. Repeat it for every table that holds user data, and grep your migrations for `using (true)`, `with check (true)` and `disable row level security`.\n\n**Q: Is it safe to expose the Supabase anon key in my frontend?**\n\nA: The anon key is designed to be public, but it is only safe when every table it can reach has RLS enabled with policies that scope rows to the caller. Without that, the anon key is a read and write key for your data.\n\n**Q: How do I fix \"new row violates row-level security policy\" without disabling RLS?**\n\nA: Make sure the inserted row's `user_id` equals `auth.uid()`, ideally by setting the column default to `auth.uid()`, and add an insert policy with `with check ((select auth.uid()) = user_id)` for the `authenticated` role.\n\n**Q: When is USING (true) acceptable?**\n\nA: Only for SELECT on tables that are genuinely public, like a list of countries or published blog posts. Never for INSERT, UPDATE or DELETE, and never on a table with user data.\n\nI've been running [SafeWeave](https://tinyurl.com/2bkvyyfs) for this. Its repo checks flag RLS being disabled (`supabase-rls-disabled`), always-true policies (` supabase-rls-passthrough-policy`) and a service_role key in a `NEXT_PUBLIC_` or `VITE_` variable, and its read-only live check runs the same anon-key test against your deployed app to find tables anyone can read. Those checks are on the Cloud plan; the free tier shows the counts. Even without a scanner, the curl test above and a grep for `using (true)` catch most of this. The important thing is catching it before your users' data is the thing that proves it.", "url": "https://wpnews.pro/news/why-cursor-fixes-supabase-rls-errors-by-opening-the-table", "canonical_source": "https://dev.to/c_k_fb750e731394/why-cursor-fixes-supabase-rls-errors-by-opening-the-table-oed", "published_at": "2026-10-11 12:42:59+00:00", "updated_at": "2026-10-11 12:51:29.532501+00:00", "lang": "en", "topics": ["ai-tools", "ai-safety", "ai-products", "developer-tools"], "entities": ["Cursor", "Supabase", "Lovable", "Matt Palmer", "CVE-2025-48757", "Postgres"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/why-cursor-fixes-supabase-rls-errors-by-opening-the-table", "markdown": "https://wpnews.pro/news/why-cursor-fixes-supabase-rls-errors-by-opening-the-table.md", "text": "https://wpnews.pro/news/why-cursor-fixes-supabase-rls-errors-by-opening-the-table.txt", "jsonld": "https://wpnews.pro/news/why-cursor-fixes-supabase-rls-errors-by-opening-the-table.jsonld"}}