← Blog
·9 min read

Your Supabase App Built with Cursor Is Probably Vulnerable

AI tools like Cursor make common Supabase RLS mistakes. Here's exactly what they get wrong, how to check your policies, and how to fix them before it matters.

Rod

Founder & Developer

Supabase cursor vulnerable is a phrase that shows up in Stack Overflow, Discord servers, and post-mortems. Not because Supabase is insecure — it's not. Because AI coding tools make predictable, consistent mistakes with Row Level Security (RLS), and those mistakes leave data exposed.

We scanned 50 Supabase repositories. 34 of them had at least one RLS misconfiguration. Of those, 17 had a configuration that would allow one authenticated user to read another user's data with a trivial query.

Here's exactly what Cursor and other AI tools get wrong — and how to check and fix each one.


What Is RLS and Why It Matters

RLS (Row Level Security) is Postgres's mechanism for enforcing data access at the database level. In Supabase, it's the security boundary between your users.

Without RLS, if User A and User B both have valid sessions, User A can query any table and get User B's data. The only thing stopping them is your application code. Application code can have bugs. RLS can't.

With RLS properly configured:

-- This policy says: users can only SELECT rows where the user_id matches their auth session
CREATE POLICY "users can read own data"
  ON user_profiles
  FOR SELECT
  USING (auth.uid() = user_id);

Even if User A constructs a malicious query or finds a bug in your API, the database itself rejects the request. That's the power of RLS. That's also why getting it wrong is serious.


The 5 RLS Mistakes AI Tools Make

Mistake #1: RLS Enabled, No Policies Defined

This is the most common mistake. Cursor enables RLS on a table — correctly — but doesn't define any policies. In Supabase, RLS with no policies means no access through the anon/authenticated role. But if your app uses the service role key for some operations, those bypass RLS entirely.

The result: your app appears to work, but there's a code path that exposes all data.

-- What Cursor often generates:
CREATE TABLE user_notes (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  user_id UUID REFERENCES auth.users NOT NULL,
  content TEXT
);
ALTER TABLE user_notes ENABLE ROW LEVEL SECURITY;
-- No policies added — Cursor stopped here
 
-- What should follow:
CREATE POLICY "users can manage own notes"
  ON user_notes
  FOR ALL
  USING (auth.uid() = user_id)
  WITH CHECK (auth.uid() = user_id);

Check it:

-- Find tables with RLS enabled but no policies
SELECT t.tablename
FROM pg_tables t
LEFT JOIN pg_policies p ON t.tablename = p.tablename
WHERE t.schemaname = 'public'
  AND t.rowsecurity = TRUE
  AND p.policyname IS NULL;

Mistake #2: Policies That Check Auth But Not Ownership

A policy that only checks auth.uid() IS NOT NULL (i.e., "is the user logged in?") allows any authenticated user to read any row. This is incorrect for multi-tenant apps where users should only see their own data.

-- BAD: Only checks authentication, not ownership
CREATE POLICY "authenticated users can read notes"
  ON user_notes
  FOR SELECT
  USING (auth.uid() IS NOT NULL);  -- any logged-in user reads all notes
 
-- GOOD: Checks ownership
CREATE POLICY "users can read own notes"
  ON user_notes
  FOR SELECT
  USING (auth.uid() = user_id);  -- users only read their own notes

Cursor generates the first pattern when prompted with something like "add RLS so only logged-in users can read." The prompt is technically satisfied — only logged-in users can read. But every logged-in user can read every row.

Mistake #3: Missing the WITH CHECK Clause on INSERT/UPDATE

The USING clause in an RLS policy filters which rows can be read. The WITH CHECK clause validates whether a row can be written. If you define a SELECT policy but forget WITH CHECK on INSERT/UPDATE, users can write rows with arbitrary user_id values.

-- BAD: Allows users to insert rows with any user_id
CREATE POLICY "users can insert notes"
  ON user_notes
  FOR INSERT
  WITH CHECK (true);  -- no ownership check on write
 
-- GOOD: Enforces that user_id matches the authenticated user
CREATE POLICY "users can insert own notes"
  ON user_notes
  FOR INSERT
  WITH CHECK (auth.uid() = user_id);

This means User A could insert a row with user_id = user_b_id. Depending on your app, this could allow impersonation or data poisoning.

Mistake #4: Service Role Key in Client-Side Code

This isn't an RLS policy mistake — it's what makes RLS irrelevant. The Supabase service role key bypasses RLS completely. It's the admin key.

AI tools sometimes generate Supabase clients using the service role key in contexts where they shouldn't — particularly in utility files that get imported by both server and client components.

// BAD: Service role key in a file that could be imported client-side
import { createClient } from "@supabase/supabase-js";
 
export const adminSupabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!  // this bypasses ALL RLS policies
);
// GOOD: Service role only in explicitly server-side files
// Use "server-only" package to prevent client imports
import "server-only";
import { createClient } from "@supabase/supabase-js";
 
export const adminSupabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
);

The server-only package causes a build error if a file is imported in a client component. It's the cheapest safeguard against accidentally exposing the service role key.

Mistake #5: Public Tables for Data That Should Be Private

Cursor sometimes generates a public access pattern for tables that seem public but shouldn't be. User profiles are a common example: you want users to read their own profile, but AI tools often make profile tables fully readable by anyone.

-- BAD: Profile table readable by anyone, including unauthenticated users
CREATE POLICY "profiles are publicly readable"
  ON profiles
  FOR SELECT
  USING (true);
 
-- GOOD: Profiles readable only by the owner (or your specific access pattern)
CREATE POLICY "users can read own profile"
  ON profiles
  FOR SELECT
  USING (auth.uid() = id);
 
-- If you genuinely need public profiles (e.g., a social app):
CREATE POLICY "public profiles are readable"
  ON profiles
  FOR SELECT
  USING (is_public = true);  -- only rows the user marked as public

How to Audit Your Supabase RLS Policies

Option 1: SQL Queries in the Supabase Dashboard

Run these in the Supabase SQL editor to check your current state:

-- Check which tables have RLS disabled (these are exposed to anyone with the anon key)
SELECT tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public'
  AND rowsecurity = FALSE;
 
-- List all existing RLS policies
SELECT tablename, policyname, permissive, roles, cmd, qual, with_check
FROM pg_policies
WHERE schemaname = 'public'
ORDER BY tablename;
 
-- Find tables with RLS enabled but zero policies (blocked entirely, possibly incorrectly)
SELECT t.tablename
FROM pg_tables t
LEFT JOIN pg_policies p ON t.tablename = p.tablename AND p.schemaname = 'public'
WHERE t.schemaname = 'public'
  AND t.rowsecurity = TRUE
  AND p.policyname IS NULL;

Option 2: Data Hogo Automated Scan

Data Hogo's database rule scanner analyzes your Supabase RLS configuration and flags the five mistake patterns above. Connect your repo, run a scan, and get findings in plain English with instructions to fix each one.

This is faster than running SQL queries if you have more than a few tables, or if you want to check both your RLS policies and your code (looking for service role key usage in client contexts).

Check your Supabase RLS policies →

See the full Supabase RLS security checklist for a complete walkthrough of policy patterns.


The Correct RLS Setup for a Typical SaaS

Here's a complete, correct RLS setup for a typical SaaS with users and their data:

-- 1. Enable RLS on every table (no exceptions)
ALTER TABLE user_profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE user_documents ENABLE ROW LEVEL SECURITY;
ALTER TABLE user_settings ENABLE ROW LEVEL SECURITY;
 
-- 2. User profiles: owner reads/updates, no deletes
CREATE POLICY "users can read own profile"
  ON user_profiles FOR SELECT
  USING (auth.uid() = id);
 
CREATE POLICY "users can update own profile"
  ON user_profiles FOR UPDATE
  USING (auth.uid() = id)
  WITH CHECK (auth.uid() = id);
 
-- 3. User documents: full CRUD, owner only
CREATE POLICY "users can manage own documents"
  ON user_documents FOR ALL
  USING (auth.uid() = user_id)
  WITH CHECK (auth.uid() = user_id);
 
-- 4. Settings: same pattern
CREATE POLICY "users can manage own settings"
  ON user_settings FOR ALL
  USING (auth.uid() = user_id)
  WITH CHECK (auth.uid() = user_id);

The pattern is consistent: every policy checks auth.uid() = <user_id_column>. Every INSERT/UPDATE policy has a WITH CHECK clause. Every table has RLS enabled.


Testing Your RLS Policies

Don't assume the policies are correct — test them. The Supabase dashboard has a RLS policy tester, or you can test directly:

-- Test as a specific user (replace with an actual user UUID)
SET LOCAL role TO 'authenticated';
SET LOCAL request.jwt.claims TO '{"sub": "user-uuid-here", "role": "authenticated"}';
 
-- This should return only rows for that user
SELECT * FROM user_documents;
 
-- This should fail or return empty for another user's document
SELECT * FROM user_documents WHERE user_id = 'another-user-uuid';

If the second query returns data, your policy has a gap.


What Most Articles Miss

One thing the standard "Supabase security" advice misses: RLS policies are evaluated differently depending on whether you're using the anon key, the authenticated role, or the service role.

  • Anon key: Subject to RLS. Policies with USING (true) allow anonymous access.
  • Authenticated role: Subject to RLS. Your user-scoped policies apply here.
  • Service role key: Bypasses RLS entirely. Always treat this as admin access.

When you test your RLS policies, make sure you're testing with the anon or authenticated role — not a service role client. Testing with the service role key will make everything appear to work even when RLS is misconfigured.

The vibe coding security risks post covers the broader pattern of how AI tools create security issues in Supabase projects and what to check.


Frequently Asked Questions

Does Cursor generate insecure Supabase RLS policies?

Not always, but frequently. AI coding tools including Cursor generate RLS-adjacent code that compiles and runs correctly but leaves data exposed. The most common mistake is creating a table and enabling RLS without defining any SELECT policy. A second common mistake is policies that check authentication status but not row ownership.

What happens if Supabase RLS is not enabled on a table?

If RLS is disabled on a Supabase table, any user with access to the anon key can read, write, or delete all rows in that table. Supabase's anon key is public — it's in your client-side code. This means your data is publicly accessible. Enabling RLS with no policies attached blocks all access by default, which is safer but may break your app if not paired with appropriate policies.

How do I check if my Supabase RLS policies are correct?

Run a Data Hogo scan against your repository — it analyzes your Supabase RLS policies and flags common mistakes. You can also run SQL queries in the Supabase SQL editor to verify: SELECT schemaname, tablename, rowsecurity FROM pg_tables WHERE schemaname = 'public'; — any table with rowsecurity = false is unprotected.

What is the difference between the Supabase anon key and service role key?

The anon key is safe to use in client-side code. It has limited permissions and is subject to RLS policies. The service role key bypasses RLS entirely — it has admin access to your entire database. The service role key must never appear in client-side code, browser environments, or anywhere it could be exposed to end users.

Can I use Supabase securely without RLS?

Only if all database access goes through server-side code that you control completely. If any client-side code queries Supabase directly using the anon key, RLS is required. Most Supabase apps use the client SDK in the browser, which means RLS is not optional — it's the primary security boundary between your users and each other's data.

supabasecursorRLSvibe-codingsecurityrow level securityAI codingdatabase security