What Is This Vulnerability
Authenticated cross-user write access occurs when RLS UPDATE or DELETE policies allow any authenticated user to modify rows belonging to other users. The policies block anonymous users but do not enforce that the authenticated user owns the rows they are modifying. This is a step above a data leak because it enables data manipulation, not just reading.
-- VULNERABLE: Any authenticated user can update ANY row
CREATE POLICY "Authenticated can update"
ON orders FOR UPDATE
TO authenticated
USING (true)
WITH CHECK (true);
Why It's Dangerous
Cross-user write access enables devastating attacks from any registered account:
- Account takeover: Modify another user's email or profile to gain control of their account
- Financial fraud: Change order amounts, payment statuses, or credit balances
- Data destruction: Delete or corrupt other users' documents, projects, or records
- Privilege escalation: Update role fields to grant admin access to the attacker's account
- Reputation damage: Modify other users' public content, reviews, or posts
Because the attacker is authenticated, every modification is tied to a valid session, making it appear as a legitimate operation in basic logging. Without detailed audit trails that track row ownership changes, these attacks can go undetected for extended periods.
// Attacker changes another user's email to take over their account
await supabase
.from('user_profiles')
.update({ email: 'attacker@evil.com' })
.eq('id', 'victim-user-id');
How to Detect
AuditYourApp tests this by authenticating as User A and attempting to update a row owned by User B. If the update succeeds, the vulnerability is confirmed.
Query the policy catalog to identify weak write policies:
SELECT tablename, policyname, cmd, qual, with_check, roles
FROM pg_policies
WHERE schemaname = 'public'
AND cmd IN ('UPDATE', 'DELETE')
AND roles @> ARRAY['authenticated']
AND (qual = 'true' OR with_check = 'true');
Also test programmatically in your development environment:
// Sign in as User A
const { data: { session } } = await supabase.auth.signInWithPassword({
email: 'usera@test.com', password: 'test123'
});
// Attempt to update User B's row
const { error } = await supabase
.from('user_profiles')
.update({ full_name: 'Hacked' })
.eq('id', 'user-b-uuid');
// If error is null, the table is vulnerable
How to Fix
Enforce ownership in both USING and WITH CHECK clauses:
DROP POLICY "Authenticated can update" ON orders;
CREATE POLICY "Users can update own orders"
ON orders FOR UPDATE
TO authenticated
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can delete own orders"
ON orders FOR DELETE
TO authenticated
USING (auth.uid() = user_id);
For admin operations, create a separate policy that checks an admin role:
CREATE POLICY "Admins can update any order"
ON orders FOR UPDATE
TO authenticated
USING (
EXISTS (
SELECT 1 FROM user_roles
WHERE user_id = auth.uid() AND role = 'admin'
)
)
WITH CHECK (true);
Combine RLS with column-level REVOKE statements to prevent modification of critical columns like user_id, role, or credit_balance even if a policy is misconfigured.
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
Authenticated User Data Leak
Authenticated users can read other users' data due to SELECT policies that do not enforce row-level ownership checks.
vulnerabilities
RLS Bypass: Unauthorized UPDATE
Tables allow unauthenticated or cross-user updates due to missing or overly permissive UPDATE policies.
vulnerabilities
RLS Bypass: Unauthorized DELETE
Tables allow unauthenticated or cross-user deletes due to missing or overly permissive DELETE policies.