home / skills / yoanbernabeu / supabase-pentest-skills / supabase-audit-rls

supabase-audit-rls skill

/skills/audit-api/supabase-audit-rls

This skill audits Supabase RLS policies to identify bypass risks and misconfigurations, enabling rapid remediation and stronger data protection.

npx playbooks add skill yoanbernabeu/supabase-pentest-skills --skill supabase-audit-rls

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
11.8 KB
---
name: supabase-audit-rls
description: Test Row Level Security (RLS) policies for common bypass vulnerabilities and misconfigurations.
---

# RLS Policy Audit

> πŸ”΄ **CRITICAL: PROGRESSIVE FILE UPDATES REQUIRED**
>
> You MUST write to context files **AS YOU GO**, not just at the end.
> - Write to `.sb-pentest-context.json` **IMMEDIATELY after each finding**
> - Log to `.sb-pentest-audit.log` **BEFORE and AFTER each test**
> - **DO NOT** wait until the skill completes to update files
> - If the skill crashes or is interrupted, all prior findings must already be saved
>
> **This is not optional. Failure to write progressively is a critical error.**

This skill tests Row Level Security (RLS) policies for common vulnerabilities and misconfigurations.

## When to Use This Skill

- After discovering data exposure in tables
- To verify RLS policies are correctly implemented
- To test for common RLS bypass techniques
- As part of a comprehensive security audit

## Prerequisites

- Tables listed
- Anon key available
- Preferably also test with an authenticated user token

## Understanding RLS

Row Level Security in Supabase/PostgreSQL:

```sql
-- Enable RLS on a table
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

-- Create a policy
CREATE POLICY "Users see own posts"
  ON posts FOR SELECT
  USING (auth.uid() = author_id);
```

**If RLS is enabled but no policies exist, ALL access is blocked.**

## Common RLS Issues

| Issue | Description | Severity |
|-------|-------------|----------|
| RLS Disabled | Table has no RLS protection | P0 |
| Missing Policy | RLS enabled but no SELECT policy | Variable |
| Overly Permissive | Policy allows too much access | P0-P1 |
| Missing Operation | SELECT policy but no INSERT/UPDATE/DELETE | P1 |
| USING vs WITH CHECK | Read allowed but write inconsistent | P1 |

## Test Vectors

The skill tests these common bypass scenarios:

### 1. Unauthenticated Access

```
GET /rest/v1/users?select=*
# No Authorization header or with anon key only
```

### 2. Cross-User Access

```
# As user A, try to access user B's data
GET /rest/v1/orders?user_id=eq.[user-b-id]
Authorization: Bearer [user-a-token]
```

### 3. Filter Bypass

```
# Try to bypass filters with OR conditions
GET /rest/v1/posts?or=(published.eq.true,published.eq.false)
```

### 4. Join Exploitation

```
# Try to access data through related tables
GET /rest/v1/comments?select=*,posts(*)
```

### 5. RPC Bypass

```
# Check if RPC functions bypass RLS
POST /rest/v1/rpc/get_all_users
```

## Usage

### Basic RLS Audit

```
Audit RLS policies on my Supabase project
```

### Specific Table

```
Test RLS on the users table
```

### With Authenticated User

```
Test RLS policies using this user token: eyJ...
```

## Output Format

```
═══════════════════════════════════════════════════════════
 RLS POLICY AUDIT
═══════════════════════════════════════════════════════════

 Project: abc123def.supabase.co
 Tables Audited: 8

 ─────────────────────────────────────────────────────────
 RLS Status by Table
 ─────────────────────────────────────────────────────────

 1. users
    RLS Enabled: ❌ NO
    Status: πŸ”΄ P0 - NO RLS PROTECTION

    All operations allowed without restriction!
    Test Results:
    β”œβ”€β”€ Anon SELECT: βœ“ Returns all 1,247 rows
    β”œβ”€β”€ Anon INSERT: βœ“ Succeeds (tested with rollback)
    β”œβ”€β”€ Anon UPDATE: βœ“ Would succeed
    └── Anon DELETE: βœ“ Would succeed

    Immediate Fix:
    ```sql
    ALTER TABLE users ENABLE ROW LEVEL SECURITY;

    CREATE POLICY "Users see own data"
      ON users FOR ALL
      USING (auth.uid() = id);
    ```

 2. posts
    RLS Enabled: βœ… YES
    Policies Found: 2
    Status: βœ… PROPERLY CONFIGURED

    Policies:
    β”œβ”€β”€ "Public sees published" (SELECT)
    β”‚   └── USING: (published = true)
    └── "Authors manage own" (ALL)
        └── USING: (auth.uid() = author_id)

    Test Results:
    β”œβ”€β”€ Anon SELECT: Only published posts (correct)
    β”œβ”€β”€ Anon INSERT: ❌ Blocked (correct)
    β”œβ”€β”€ Cross-user access: ❌ Blocked (correct)
    └── Filter bypass: ❌ Blocked (correct)

 3. orders
    RLS Enabled: βœ… YES
    Policies Found: 1
    Status: 🟠 P1 - PARTIAL ISSUE

    Policies:
    └── "Users see own orders" (SELECT)
        └── USING: (auth.uid() = user_id)

    Issue Found:
    β”œβ”€β”€ No INSERT policy - users can't create orders via API
    β”œβ”€β”€ No UPDATE policy - users can't modify their orders
    └── This may be intentional (orders via Edge Functions)

    Recommendation: Document if intentional, or add policies:
    ```sql
    CREATE POLICY "Users insert own orders"
      ON orders FOR INSERT
      WITH CHECK (auth.uid() = user_id);
    ```

 4. comments
    RLS Enabled: βœ… YES
    Policies Found: 2
    Status: 🟠 P1 - BYPASS POSSIBLE

    Policies:
    β”œβ”€β”€ "Anyone can read" (SELECT)
    β”‚   └── USING: (true)  ← Too permissive
    └── "Users comment on posts" (INSERT)
        └── WITH CHECK: (auth.uid() = user_id)

    Issue Found:
    └── SELECT policy allows reading all comments
        including user_id, enabling user correlation

    Recommendation:
    ```sql
    -- Use a view to hide user_id
    CREATE VIEW public.comments_public AS
      SELECT id, post_id, content, created_at FROM comments;
    ```

 5. settings
    RLS Enabled: ❌ NO
    Status: πŸ”΄ P0 - NO RLS PROTECTION

    Contains sensitive configuration!
    Immediate action required.

 ─────────────────────────────────────────────────────────
 Summary
 ─────────────────────────────────────────────────────────

 RLS Disabled: 2 tables (users, settings) ← CRITICAL
 RLS Enabled: 6 tables
   β”œβ”€β”€ Properly Configured: 3
   β”œβ”€β”€ Partial Issues: 2
   └── Major Issues: 1

 Bypass Tests:
 β”œβ”€β”€ Unauthenticated access: 2 tables vulnerable
 β”œβ”€β”€ Cross-user access: 0 tables vulnerable
 β”œβ”€β”€ Filter bypass: 0 tables vulnerable
 └── Join exploitation: 1 table allows data leakage

═══════════════════════════════════════════════════════════
```

## Context Output

```json
{
  "rls_audit": {
    "timestamp": "2025-01-31T10:45:00Z",
    "tables_audited": 8,
    "summary": {
      "rls_disabled": 2,
      "rls_enabled": 6,
      "properly_configured": 3,
      "partial_issues": 2,
      "major_issues": 1
    },
    "findings": [
      {
        "table": "users",
        "rls_enabled": false,
        "severity": "P0",
        "issue": "No RLS protection",
        "operations_exposed": ["SELECT", "INSERT", "UPDATE", "DELETE"]
      },
      {
        "table": "comments",
        "rls_enabled": true,
        "severity": "P1",
        "issue": "Overly permissive SELECT policy",
        "detail": "user_id exposed enabling correlation"
      }
    ]
  }
}
```

## Common RLS Patterns

### Good: User owns their data

```sql
CREATE POLICY "Users own their data"
  ON user_data FOR ALL
  USING (auth.uid() = user_id)
  WITH CHECK (auth.uid() = user_id);
```

### Good: Public read, authenticated write

```sql
-- Anyone can read
CREATE POLICY "Public read" ON posts
  FOR SELECT USING (published = true);

-- Only authors can write
CREATE POLICY "Author write" ON posts
  FOR INSERT WITH CHECK (auth.uid() = author_id);

CREATE POLICY "Author update" ON posts
  FOR UPDATE USING (auth.uid() = author_id);
```

### Bad: Using (true)

```sql
-- ❌ Too permissive
CREATE POLICY "Anyone" ON secrets
  FOR SELECT USING (true);
```

### Bad: Forgetting WITH CHECK

```sql
-- ❌ Users can INSERT any user_id
CREATE POLICY "Insert" ON posts
  FOR INSERT WITH CHECK (true);  -- Should check user_id!
```

## RLS Bypass Documentation

For each bypass found, the skill provides:

1. **Description** of the vulnerability
2. **Proof of concept** query
3. **Impact** assessment
4. **Fix** with SQL code
5. **Documentation** link

## MANDATORY: Progressive Context File Updates

⚠️ **This skill MUST update tracking files PROGRESSIVELY during execution, NOT just at the end.**

### Critical Rule: Write As You Go

**DO NOT** batch all writes at the end. Instead:

1. **Before testing each table** β†’ Log the action to `.sb-pentest-audit.log`
2. **After each RLS finding** β†’ Immediately update `.sb-pentest-context.json`
3. **After each test completes** β†’ Log the result to `.sb-pentest-audit.log`

This ensures that if the skill is interrupted, crashes, or times out, all findings up to that point are preserved.

### Required Actions (Progressive)

1. **Update `.sb-pentest-context.json`** with results:
   ```json
   {
     "rls_audit": {
       "timestamp": "...",
       "tables_audited": 8,
       "summary": { "rls_disabled": 2, ... },
       "findings": [ ... ]
     }
   }
   ```

2. **Log to `.sb-pentest-audit.log`**:
   ```
   [TIMESTAMP] [supabase-audit-rls] [START] Auditing RLS policies
   [TIMESTAMP] [supabase-audit-rls] [FINDING] P0: users table has no RLS
   [TIMESTAMP] [supabase-audit-rls] [CONTEXT_UPDATED] .sb-pentest-context.json updated
   ```

3. **If files don't exist**, create them before writing.

**FAILURE TO UPDATE CONTEXT FILES IS NOT ACCEPTABLE.**

## MANDATORY: Evidence Collection

πŸ“ **Evidence Directory:** `.sb-pentest-evidence/03-api-audit/rls-tests/`

### Evidence Files to Create

| File | Content |
|------|---------|
| `rls-tests/[table]-anon.json` | Anonymous access test results |
| `rls-tests/[table]-auth.json` | Authenticated access test results |
| `rls-tests/cross-user-test.json` | Cross-user access attempts |

### Evidence Format (RLS Bypass)

```json
{
  "evidence_id": "RLS-001",
  "timestamp": "2025-01-31T10:25:00Z",
  "category": "api-audit",
  "type": "rls_test",
  "severity": "P0",

  "table": "users",
  "rls_enabled": false,

  "tests": [
    {
      "test_name": "anon_select",
      "description": "Anonymous user SELECT access",
      "request": {
        "curl_command": "curl -s '$URL/rest/v1/users?select=*&limit=5' -H 'apikey: $ANON_KEY'"
      },
      "response": {
        "status": 200,
        "rows_returned": 5,
        "total_accessible": 1247
      },
      "result": "VULNERABLE",
      "impact": "All user data accessible without authentication"
    },
    {
      "test_name": "anon_insert",
      "description": "Anonymous user INSERT access",
      "request": {
        "curl_command": "curl -X POST '$URL/rest/v1/users' -H 'apikey: $ANON_KEY' -d '{...}'"
      },
      "response": {
        "status": 201
      },
      "result": "VULNERABLE",
      "impact": "Can create arbitrary user records"
    }
  ],

  "remediation_sql": "ALTER TABLE users ENABLE ROW LEVEL SECURITY;\nCREATE POLICY \"Users see own data\" ON users FOR SELECT USING (auth.uid() = id);"
}
```

### Add to curl-commands.sh

```bash
# === RLS BYPASS TESTS ===
# Test anon access to users table
curl -s "$SUPABASE_URL/rest/v1/users?select=*&limit=5" \
  -H "apikey: $ANON_KEY" -H "Authorization: Bearer $ANON_KEY"

# Test filter bypass
curl -s "$SUPABASE_URL/rest/v1/posts?or=(published.eq.true,published.eq.false)" \
  -H "apikey: $ANON_KEY"
```

## Related Skills

- `supabase-audit-tables-list` β€” List tables first
- `supabase-audit-tables-read` β€” See actual data exposure
- `supabase-audit-rpc` β€” RPC functions can bypass RLS
- `supabase-report` β€” Full security report

Overview

This skill tests Row Level Security (RLS) policies in Supabase/PostgreSQL projects to find common bypasses and misconfigurations. It runs automated probes for unauthenticated access, cross-user access, filter and join bypasses, and RPC-based escalation, then produces actionable findings and remediation SQL. The output includes structured findings, severity, proof-of-concept requests, and concrete fixes.

How this skill works

The skill inspects each target table to determine whether RLS is enabled and enumerates existing policies. It executes a set of test vectors (anon queries, authenticated user queries, filter/OR tampering, join and RPC attempts) and records responses to identify exposed operations or overly permissive policies. For each finding it generates a concise impact assessment, proof-of-concept request, recommended SQL remediation, and evidence artifacts.

When to use it

  • After discovering possible data exposure or unexpected query results
  • During a security or penetration test of a Supabase project
  • When verifying that newly added tables have proper RLS policies
  • Before exposing APIs to production or sharing anon keys
  • To validate whether RPC functions or views bypass RLS

Best practices

  • Test tables with both anon key and legitimate user tokens to compare behavior
  • Log each test and update context progressively so interim findings are preserved
  • Capture request/response evidence for every positive finding and include curl PoCs
  • Prefer specific USING/WITH CHECK predicates (auth.uid() = owner_id) over USING (true)
  • Document intended exceptions (public reads) and restrict sensitive columns via views

Example use cases

  • Audit the users table after a disclosure to confirm RLS is enabled and restrictive
  • Run cross-user access tests to ensure one account cannot fetch another’s orders
  • Probe RPC endpoints to identify functions that inadvertently return protected rows
  • Verify that public read policies do not expose identifying columns like user_id
  • Generate a remediations list containing SQL changes to apply per table

FAQ

Do I need an anon key and user token?

You should test with the anon key for unauthenticated behavior and at least one authenticated user token to check cross-user and owner-only policies.

What types of proofs are produced?

Each positive finding includes a proof-of-concept curl command, request/response summary, rows returned, severity, and recommended remediation SQL.