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

supabase-audit-authenticated skill

/skills/audit-auth/supabase-audit-authenticated

This skill creates a test authenticated user to audit access gaps versus anonymous users and detect IDOR, cross-user access, and privilege escalation.

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

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

Files (1)
SKILL.md
17.2 KB
---
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

Overview

This skill creates a disposable test user with explicit consent and runs authenticated-vs-anonymous checks to detect IDOR, cross-user access, and privilege escalation in Supabase projects. It captures evidence and saves progressive context and logs as each test completes. The goal is to reveal vulnerabilities that only appear for authenticated users and produce actionable findings and remediation guidance.

How this skill works

After explicit authorization, the skill generates a strong random password and a pentest-[8char] email, signs up the test user via the Supabase auth endpoint, and obtains a JWT. It then performs table, storage, realtime, and function checks as both anonymous and authenticated users, comparing results to identify auth-only issues like IDOR or overly permissive RLS. All actions, responses, and evidence files are written incrementally to audit logs and context files so results are preserved if the run is interrupted.

When to use it

  • After anonymous access testing to reveal auth-only vulnerabilities
  • To detect IDORs where objects are accessible by any authenticated user
  • To verify RLS policies enforce per-user ownership
  • To confirm storage and realtime channels do not expose other users' data
  • Before production deployments to catch privilege escalation issues

Best practices

  • Obtain explicit consent before creating test users and document it in the log
  • Write context and evidence files immediately after each action (never batch at end)
  • Use a non-deliverable test email domain and store the password securely (shown once)
  • Compare identical queries for anon and auth to highlight differences
  • Remove the test user after the audit using service_role or dashboard cleanup

Example use cases

  • Sign up a test user and confirm whether the users table leaks every account to any auth user
  • As the test user, attempt to GET another user's orders by filtering user_id to detect IDOR
  • List storage bucket objects authenticated vs anonymous to find overly broad access
  • Subscribe to realtime events as authenticated user to see if other users' events are delivered
  • Attempt to call admin functions with a regular test user to detect privilege escalation

FAQ

Do I need to approve creating a test user?

Yes. This skill requires explicit consent before creating the test account; it will not proceed without your authorization.

What happens if the audit is interrupted?

All critical outputs and findings are written progressively to the audit log and context files so partial results remain available.