What Is This Vulnerability
Public table write access occurs when a table's RLS INSERT, UPDATE, or DELETE policies allow the anon role to modify data. This means anyone on the internet can write to your database without creating an account or authenticating. While there are rare legitimate use cases (e.g., anonymous feedback forms), this is almost always a critical misconfiguration.
-- VULNERABLE: Anonymous users can insert rows
CREATE POLICY "Anyone can insert"
ON feedback FOR INSERT
WITH CHECK (true);
-- VULNERABLE: Anonymous users can update rows
CREATE POLICY "Anyone can update"
ON feedback FOR UPDATE
USING (true)
WITH CHECK (true);
Why It's Dangerous
Public write access is significantly more dangerous than public read access because it allows attackers to:
- Inject malicious data: Insert XSS payloads, phishing links, or spam content
- Overwrite legitimate data: Modify existing records, corrupting your database
- Exhaust resources: Insert millions of rows, consuming storage and increasing costs
- Create fake accounts or records: Bypass registration flows by directly inserting into user tables
- Manipulate business logic: Alter prices, quantities, statuses, or permissions
- Delete data: Remove critical records if DELETE is also open
All of this can be automated with simple scripts, meaning an attacker can cause massive damage in seconds:
# Insert spam data as anonymous user
curl -X POST 'https://YOUR_PROJECT.supabase.co/rest/v1/feedback' \
-H "apikey: YOUR_ANON_KEY" \
-H "Content-Type: application/json" \
-d '{"content": "malicious payload", "user_id": "fake-uuid"}'
How to Detect
Test write operations as an anonymous user:
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(SUPABASE_URL, ANON_KEY);
// Test anonymous insert
const { error } = await supabase.from('feedback').insert({ content: 'test' });
if (!error) console.log('TABLE IS WRITABLE BY ANONYMOUS USERS');
Query policies to find write permissions for the anon role:
SELECT tablename, policyname, cmd, roles
FROM pg_policies
WHERE schemaname = 'public'
AND cmd IN ('INSERT', 'UPDATE', 'DELETE')
AND (roles @> ARRAY['anon'] OR roles @> ARRAY['public']);
How to Fix
Restrict write policies to authenticated users at minimum:
DROP POLICY "Anyone can insert" ON feedback;
DROP POLICY "Anyone can update" ON feedback;
-- Only authenticated users can insert, and only their own rows
CREATE POLICY "Authenticated users can insert own feedback"
ON feedback FOR INSERT
TO authenticated
WITH CHECK (auth.uid() = user_id);
-- Only owners can update their feedback
CREATE POLICY "Users can update own feedback"
ON feedback FOR UPDATE
TO authenticated
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
For truly anonymous submissions (e.g., a contact form), consider using an Edge Function as a proxy instead of direct table access. The Edge Function can validate input, apply rate limiting, and insert using the service_role key:
// Edge Function approach for anonymous submissions
Deno.serve(async (req) => {
const { message, email } = await req.json();
// Validate and rate-limit here
const { error } = await supabaseAdmin
.from('contact_submissions')
.insert({ message, email });
return new Response(JSON.stringify({ success: !error }));
});
Scan your app for this vulnerability
AuditYourApp automatically detects security misconfigurations in Supabase and Firebase projects. Get actionable remediation in minutes.
Run Free ScanRelated
vulnerabilities
Public Table Read Access
Tables are readable by anonymous users through the Supabase API, potentially exposing sensitive data to unauthenticated visitors.
vulnerabilities
Missing Row Level Security Policy
Tables without RLS are fully exposed to any user with the anon key, allowing unrestricted read and write access to all rows.
vulnerabilities
RLS Bypass: Unauthorized INSERT
Tables allow unauthenticated or cross-user inserts due to missing or overly permissive INSERT policies.