Skip to main content
skill CX Codex

supabase-audit-authenticated — Claude Code skill: authenticated

Creates a test user in Supabase and compares what an authenticated account can reach against anonymous access. Finds IDOR, cross-user reads and privilege escalation.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add yoanbernabeu/supabase-pentest-skills --skill supabase-audit-authenticated -a claude-code
Open on GitHub

What supabase-audit-authenticated does

The skill signs up a test user in your Supabase project with an address like pentest-[random]@security-audit.local and a random password of 32+ characters. It then logs in, grabs the JWT, and walks your tables, storage and Realtime channels: how many rows anonymous gets, how many the authenticated user gets, and whether that user sees only their own records.

IDOR testing is a separate pass. The skill requests rows by someone else's user_id and by a not-equal filter, and any rows coming back count as a finding. It writes to .sb-pentest-context.json and .sb-pentest-audit.log as it goes rather than at the end, so an interrupted run still leaves the findings on disk.

You have to authorize the test user first: the skill waits for an explicit confirmation and does nothing without it. Signup must be open, or you need an invite flow, plus an anon key. At the end it asks whether to delete the test user. If signup is closed in your project, the audit cannot start — worth checking before you run it.

What it helps with

  • compare row counts a table returns to anonymous vs authenticated callers
  • catch IDOR when querying rows by another user's user_id
  • check whether a not-equal filter leaks other users' profiles
  • verify RLS lets an authenticated user read only their own records
  • test storage object listing and Realtime subscriptions under a test account

Example prompts for the agent

  • create a test user and compare authenticated access with anonymous
  • run the IDOR audit on the orders table
  • check whether the test user can see other people's profiles in profiles

How to install supabase-audit-authenticated

1Skills CLI (Vercel)

npx skills add yoanbernabeu/supabase-pentest-skills --skill supabase-audit-authenticated -a claude-code

2Manually: put the files in place

~/.claude/skills/supabase-audit-authenticated/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/supabase-audit-authenticated/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about supabase-audit-authenticated

How do I install supabase-audit-authenticated in Claude?

Run npx skills add yoanbernabeu/supabase-pentest-skills --skill supabase-audit-authenticated -a claude-code. The skill file then lives at ~/.claude/skills/supabase-audit-authenticated/SKILL.md.

What does supabase-audit-authenticated do?

It creates a test user in Supabase and compares authenticated access against anonymous access. It looks for IDOR, cross-user data exposure and privilege escalation.

What do I need before running it?

Explicit consent to create the test user, open signup or an invite flow, and an anon key. Running the anonymous audit first is recommended.

Source text

SKILL.md · MIT
---
name: supabase-audit-authenticated
description: Create a test user (with explicit permission) to audit what authenticated users can access vs anonymous users. Detects IDOR, cross-user access, and privilege escalation.
---

# Authenticated User 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 test**
> - Log to `.sb-pentest-audit.log` **BEFORE and AFTER each action**
> - **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 creates a test user (with explicit permission) to compare authenticated vs anonymous access and detect IDOR vulnerabilities.

## ⚠️ IMPORTANT: User Consent Required

```
╔═══════════════════════════════════════════════════════════════════╗
║  🔐 USER CREATION CONSENT REQUIRED                                ║
╠═══════════════════════════════════════════════════════════════════╣
║                                                                   ║
║  This skill will CREATE A TEST USER in your Supabase project.     ║
║                                                                   ║
║  The user will be created with:                                   ║
║  • Email: pentest-[random]@security-audit.local                   ║
║  • Password: Strong random password (32+ chars)                   ║
║  • Purpose: Testing authenticated access vs anonymous             ║
║                                                                   ║
║  At the end of the audit, you will be asked if you want to        ║
║  DELETE the test user (recommended).                              ║
║                                                                   ║
║  Do you authorize the creation of a test user?                    ║
║  Type "yes, create test user" to proceed.                         ║
║                                                                   ║
╚═══════════════════════════════════════════════════════════════════╝
```

**DO NOT proceed without explicit user consent.**

## When to Use This Skill

- After completing anonymous access tests
- To detect IDOR (Insecure Direct Object Reference) vulnerabilities
- To test cross-user data access
- To verify RLS policies work for authenticated users
- To find privilege escalation issues

## Prerequisites

- Signup must be open (or use invite flow)
- Anon key available
- Anonymous audit completed (recommended)

## Why Authenticated Testing Matters

Many vulnerabilities only appear with authentication:

| Vulnerability | Anonymous | Authenticated |
|---------------|-----------|---------------|
| **RLS bypass (no RLS)** | ✓ Detectable | ✓ Detectable |
| **IDOR** | ✗ Not visible | ✓ **Only visible** |
| **Cross-user access** | ✗ Not visible | ✓ **Only visible** |
| **Privilege escalation** | ✗ Not visible | ✓ **Only visible** |
| **Overly permissive RLS** | Partial | ✓ **Full detection** |

## Test User Creation

### Email Format

```
pentest-[8-char-random]@security-audit.local
```

Example: `[email protected]`

### Password Generation

Strong password with:
- 32+ characters
- Uppercase, lowercase, numbers, symbols
- Cryptographically random

Example: `Xk9$mP2#vL5@nQ8&jR4*wY7!hT3%bU6^`

**The password is displayed ONCE and saved to evidence.**

## Tests Performed

### 1. User Creation & Login

```bash
# Create user
curl -X POST "$SUPABASE_URL/auth/v1/signup" \
  -H "apikey: $ANON_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email": "[email protected]", "password": "[STRONG_PASSWORD]"}'

# Login and get JWT
curl -X POST "$SUPABASE_URL/auth/v1/token?grant_type=password" \
  -H "apikey: $ANON_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email": "[email protected]", "password": "[STRONG_PASSWORD]"}'
```

### 2. Authenticated vs Anonymous Comparison

For each table:

| Test | Anonymous | Authenticated | Finding |
|------|-----------|---------------|---------|
| SELECT | 0 rows | 1,247 rows | 🔴 Auth-only exposure |
| Own data | N/A | Only own row | ✅ RLS working |
| Other users' data | N/A | All rows | 🔴 Cross-user access |

### 3. IDOR Testing

```bash
# As test user, try to access other user's data
curl "$SUPABASE_URL/rest/v1/orders?user_id=eq.[OTHER_USER_ID]" \
  -H "apikey: $ANON_KEY" \
  -H "Authorization: Bearer [TEST_USER_JWT]"

# If returns data: IDOR vulnerability!
```

### 4. Cross-User Access

```bash
# Get test user's ID from JWT
TEST_USER_ID=$(echo $JWT | jq -r '.sub')

# Try to access data belonging to a different user
curl "$SUPABASE_URL/rest/v1/profiles?id=neq.$TEST_USER_ID" \
  -H "Authorization: Bearer [TEST_USER_JWT]"

# If returns other users' profiles: Cross-user access!
```

### 5. Storage with Authentication

```bash
# Test authenticated storage access
curl "$SUPABASE_URL/storage/v1/object/list/documents" \
  -H "apikey: $ANON_KEY" \
  -H "Authorization: Bearer [TEST_USER_JWT]"

# Compare with anonymous results
```

### 6. Realtime with Authentication

```javascript
// Subscribe to table changes as authenticated user
const channel = supabase.channel('test')
  .on('postgres_changes', {
    event: '*',
    schema: 'public',
    table: 'orders'
  }, payload => console.log(payload))
  .subscribe()

// Does it receive OTHER users' order changes?
```

## Output Format

```
═══════════════════════════════════════════════════════════
 AUTHENTICATED USER AUDIT
═══════════════════════════════════════════════════════════

 ─────────────────────────────────────────────────────────
 Test User Creation
 ─────────────────────────────────────────────────────────

 Status: ✅ User created successfully

 Test User Details:
 ├── Email: [email protected]
 ├── User ID: 550e8400-e29b-41d4-a716-446655440099
 ├── Password: [Saved to evidence - shown once]
 └── JWT obtained: ✅

 ─────────────────────────────────────────────────────────
 Anonymous vs Authenticated Comparison
 ─────────────────────────────────────────────────────────

 Table: users
 ├── Anonymous access: 0 rows
 ├── Authenticated access: 1,247 rows ← ALL USERS!
 └── Status: 🔴 P0 - Data hidden from anon but exposed to any auth user

 Table: orders
 ├── Anonymous access: 0 rows (blocked)
 ├── Authenticated access: 1 row (own orders only)
 └── Status: ✅ RLS working correctly

 Table: profiles
 ├── Anonymous access: 0 rows
 ├── Authenticated access: 1,247 rows ← ALL PROFILES!
 ├── Own profile only expected: ❌ NO
 └── Status: 🔴 P0 - Cross-user profile access

 ─────────────────────────────────────────────────────────
 IDOR Testing
 ─────────────────────────────────────────────────────────

 Test: Access other user's orders by ID
 ├── Request: GET /orders?user_id=eq.[other-user-id]
 ├── Auth: Test user JWT
 ├── Response: 200 OK - 15 orders returned
 └── Status: 🔴 P0 - IDOR VULNERABILITY

 Proof:
 curl "$URL/rest/v1/orders?user_id=eq.other-user-uuid" \
   -H "Authorization: Bearer [test-user-jwt]"
 # Returns orders belonging to other-user-uuid!

 Test: Access admin endpoints
 ├── Request: GET /functions/v1/admin-panel
 ├── Auth: Test user JWT (regular user)
 ├── Response: 200 OK - Admin data returned!
 └── Status: 🔴 P0 - PRIVILEGE ESCALATION

 ─────────────────────────────────────────────────────────
 Storage with Authentication
 ─────────────────────────────────────────────────────────

 Bucket: documents
 ├── Anonymous: ❌ 0 files (blocked)
 ├── Authenticated: ✅ 523 files visible ← ALL USERS' FILES!
 └── Status: 🔴 P1 - Auth users see all documents

 Bucket: user-uploads
 ├── Anonymous: ❌ 0 files
 ├── Authenticated: 3 files (own files only)
 └── Status: ✅ RLS working correctly

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

 New Findings (Auth-only):
 ├── 🔴 P0: users table - all users visible to any auth user
 ├── 🔴 P0: profiles table - cross-user access
 ├── 🔴 P0: IDOR in orders - can access any user's orders
 ├── 🔴 P0: Privilege escalation in admin-panel
 └── 🟠 P1: documents bucket - all files visible to auth users

 Comparison:
 ├── Issues found (Anonymous): 3
 ├── Issues found (Authenticated): 8 ← 5 NEW ISSUES!
 └── Auth-only vulnerabilities: 5

 Recommendation:
 These issues were NOT visible in anonymous testing!
 Always test with authenticated users.

 ─────────────────────────────────────────────────────────
 Cleanup
 ─────────────────────────────────────────────────────────

 ⚠️  Test user still exists in database.

 Do you want to delete the test user?
 Email: [email protected]

 [This requires service_role key or manual deletion]

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

## Context Output

```json
{
  "authenticated_audit": {
    "timestamp": "2025-01-31T12:00:00Z",
    "test_user": {
      "email": "[email protected]",
      "user_id": "550e8400-e29b-41d4-a716-446655440099",
      "created_at": "2025-01-31T12:00:00Z",
      "deleted": false
    },
    "comparison": {
      "tables": {
        "users": {
          "anon_access": 0,
          "auth_access": 1247,
          "expected_auth_access": "own_row_only",
          "severity": "P0",
          "finding": "All users visible to any authenticated user"
        },
        "orders": {
          "anon_access": 0,
          "auth_access": 1,
          "expected_auth_access": "own_rows_only",
          "severity": null,
          "finding": "RLS working correctly"
        }
      },
      "idor_tests": [
        {
          "test": "access_other_user_orders",
          "vulnerable": true,
          "severity": "P0",
          "proof": "curl command..."
        }
      ],
      "privilege_escalation": [
        {
          "endpoint": "/functions/v1/admin-panel",
          "vulnerable": true,
          "severity": "P0"
        }
      ]
    },
    "summary": {
      "anon_issues": 3,
      "auth_issues": 8,
      "auth_only_issues": 5
    }
  }
}
```

## RLS Policy Examples

### Correct: Users see only their own data

```sql
-- This RLS policy is correct
CREATE POLICY "Users see own data"
  ON users FOR SELECT
  USING (auth.uid() = id);

-- Result:
-- Anonymous: 0 rows
-- Authenticated: 1 row (own data)
```

### Incorrect: All authenticated users see everything

```sql
-- This RLS policy is WRONG
CREATE POLICY "Authenticated users see all"
  ON users FOR SELECT
  USING (auth.role() = 'authenticated');  -- ❌ Too permissive!

-- Result:
-- Anonymous: 0 rows
-- Authenticated: ALL rows ← VULNERABILITY!
```

### Correct fix:

```sql
-- Fix: Add user ownership check
CREATE POLICY "Users see own data"
  ON users FOR SELECT
  USING (auth.uid() = id);  -- ✅ Only own row
```

## Cleanup Options

### Option 1: Manual deletion (Dashboard)

```
Supabase Dashboard → Authentication → Users → Find test user → Delete
```

### Option 2: Via service_role key (if available)

```bash
curl -X DELETE "$SUPABASE_URL/auth/v1/admin/users/[USER_ID]" \
  -H "apikey: $SERVICE_ROLE_KEY" \
  -H "Authorization: Bearer $SERVICE_ROLE_KEY"
```

### Option 3: Leave for later

The test user uses a non-functional email domain (`security-audit.local`) and cannot be used maliciously.

## MANDATORY: Evidence Collection

📁 **Evidence Directory:** `.sb-pentest-evidence/05-auth-audit/authenticated-tests/`

### Evidence Files to Create

| File | Content |
|------|---------|
| `test-user-created.json` | Test user details (password saved securely) |
| `anon-vs-auth-comparison.json` | Side-by-side comparison |
| `idor-tests/[table].json` | IDOR test results |
| `privilege-escalation.json` | Privilege escalation tests |

### Evidence Format

```json
{
  "evidence_id": "AUTH-TEST-001",
  "timestamp": "2025-01-31T12:00:00Z",
  "category": "auth-audit",
  "type": "authenticated_testing",

  "test_user": {
    "email": "[email protected]",
    "user_id": "550e8400-...",
    "password": "[STORED SECURELY - DO NOT COMMIT]"
  },

  "comparison_test": {
    "table": "users",
    "anonymous": {
      "curl_command": "curl '$URL/rest/v1/users' -H 'apikey: $ANON_KEY'",
      "response_status": 200,
      "rows_returned": 0
    },
    "authenticated": {
      "curl_command": "curl '$URL/rest/v1/users' -H 'apikey: $ANON_KEY' -H 'Authorization: Bearer $JWT'",
      "response_status": 200,
      "rows_returned": 1247
    },
    "finding": {
      "severity": "P0",
      "issue": "All users visible to any authenticated user",
      "expected": "Only own row should be visible",
      "impact": "Full user enumeration for any authenticated user"
    }
  }
}
```

### Add to curl-commands.sh

```bash
# === AUTHENTICATED TESTING ===
# NOTE: Replace [JWT] with test user's JWT

# Compare anonymous vs authenticated access
curl -s "$SUPABASE_URL/rest/v1/users?select=*&limit=5" -H "apikey: $ANON_KEY"
curl -s "$SUPABASE_URL/rest/v1/users?select=*&limit=5" -H "apikey: $ANON_KEY" -H "Authorization: Bearer [JWT]"

# IDOR test - access other user's data
curl -s "$SUPABASE_URL/rest/v1/orders?user_id=eq.[OTHER_USER_ID]" \
  -H "apikey: $ANON_KEY" \
  -H "Authorization: Bearer [JWT]"

# Cross-user profile access
curl -s "$SUPABASE_URL/rest/v1/profiles?id=neq.[TEST_USER_ID]" \
  -H "apikey: $ANON_KEY" \
  -H "Authorization: Bearer [JWT]"
```

## 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 user creation** → Log consent and action to `.sb-pentest-audit.log`
2. **After user created** → Immediately save user details to context and evidence
3. **After each comparison test** → Update `.sb-pentest-context.json` with results
4. **After each IDOR test** → Save evidence immediately

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

### Required Actions (Progressive)

1. **Log user creation:**
   ```
   [TIMESTAMP] [supabase-audit-authenticated] [CONSENT] User authorized test user creation
   [TIMESTAMP] [supabase-audit-authenticated] [CREATED] Test user [email protected]
   ```

2. **Save test user to context immediately:**
   ```json
   {
     "authenticated_audit": {
       "test_user": {
         "email": "...",
         "user_id": "...",
         "created_at": "..."
       }
     }
   }
   ```

3. **Log each finding as discovered:**
   ```
   [TIMESTAMP] [supabase-audit-authenticated] [FINDING] P0: IDOR in orders table
   ```

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

## Related Skills

- `supabase-audit-auth-signup` — Test if signup is open first
- `supabase-audit-tables-read` — Compare with anonymous results
- `supabase-audit-rls` — Deep dive into RLS policies
- `supabase-audit-functions` — Test function access with auth
- `supabase-report` — Include auth-only findings in report

© yoanbernabeu · MIT · yoanbernabeu/supabase-pentest-skills

Similar skills

Everything for Security →

From the same set11

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor