SupabaseMedium

Authenticated User Data Leak

Authenticated users can read other users' data due to SELECT policies that do not enforce row-level ownership checks.

Last updated 2026-01-15

What Is This Vulnerability

An authenticated data leak occurs when RLS SELECT policies allow any authenticated user to read all rows in a table, rather than restricting access to rows they own. The policy correctly excludes anonymous users but fails to differentiate between authenticated users. This means User A can read User B's data simply by being logged in.

-- VULNERABLE: Any logged-in user can read ALL rows
CREATE POLICY "Authenticated can read"
  ON user_profiles FOR SELECT
  TO authenticated
  USING (true);

This is a common pattern when developers focus on blocking anonymous access but forget that authenticated users also need row-level restrictions. It creates a false sense of security because anonymous access is properly blocked.

Why It's Dangerous

In a multi-tenant application, this vulnerability allows any registered user to:

  • Enumerate all users: Read every profile, email address, and personal detail in the system
  • Access private content: View other users' private documents, messages, or uploads
  • Harvest competitive data: Extract business data, pricing, or customer lists
  • Facilitate social engineering: Use leaked personal information for targeted phishing attacks

Since the attacker must be authenticated, the barrier is slightly higher than public access. However, account creation is usually trivial (sign up with any email), making this only a minor obstacle. In many Supabase projects, email verification is not enforced, so an attacker can create throwaway accounts instantly.

// Any authenticated user can dump all profiles
const { data } = await supabase
  .from('user_profiles')
  .select('id, email, full_name, phone, address');
// Returns ALL users' data, not just the caller's

How to Detect

AuditYourApp tests this by creating two test users and attempting to read data owned by User A while authenticated as User B. If the query returns User A's rows, the policy is overly permissive.

You can manually check by inspecting policies:

SELECT tablename, policyname, cmd, qual, roles
FROM pg_policies
WHERE schemaname = 'public'
  AND cmd = 'SELECT'
  AND roles @> ARRAY['authenticated']
  AND qual = 'true';

Any SELECT policy for the authenticated role with a qual of true is a likely finding, unless the table is intentionally shared (e.g., a public directory).

How to Fix

Add ownership checks to SELECT policies:

DROP POLICY "Authenticated can read" ON user_profiles;

-- Users can only read their own profile
CREATE POLICY "Users can read own profile"
  ON user_profiles FOR SELECT
  TO authenticated
  USING (auth.uid() = id);

For tables where some data should be shared (e.g., a user directory showing names but not emails), create separate policies or use column-level security:

-- Create a public view with limited columns
CREATE VIEW public_profiles AS
  SELECT id, display_name, avatar_url
  FROM user_profiles;

-- Or use multiple policies with different scopes
CREATE POLICY "Users can read own full profile"
  ON user_profiles FOR SELECT
  TO authenticated
  USING (auth.uid() = id);

CREATE POLICY "Users can read others display info"
  ON user_profiles FOR SELECT
  TO authenticated
  USING (visibility = 'public');

Always consider whether the query pattern your client uses actually needs all rows or just the current user's rows. Structure your RLS to match the minimum access required.

Scan your app for this vulnerability

AuditYourApp automatically detects security misconfigurations in Supabase and Firebase projects. Get actionable remediation in minutes.

Run Free Scan