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

supabase-audit-rpc skill

/skills/audit-api/supabase-audit-rpc

This skill audits Supabase RPC functions for security issues, testing RLS bypass, SQL injection, and exposure risks to deliver actionable findings.

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

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

Files (1)
SKILL.md
11.5 KB
---
name: supabase-audit-rpc
description: List and test exposed PostgreSQL RPC functions for security issues and potential RLS bypass.
---

# RPC Functions 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 function tested**
> - Log to `.sb-pentest-audit.log` **BEFORE and AFTER each function 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 discovers and tests PostgreSQL functions exposed via Supabase's RPC endpoint.

## When to Use This Skill

- To discover exposed database functions
- To test if functions bypass RLS
- To check for SQL injection in function parameters
- As part of comprehensive API security testing

## Prerequisites

- Supabase URL and anon key available
- Tables audit completed (recommended)

## Understanding Supabase RPC

Supabase exposes PostgreSQL functions via:

```
POST https://[project].supabase.co/rest/v1/rpc/[function_name]
```

Functions can:
- āœ… Respect RLS (if using `auth.uid()` and proper security)
- āŒ Bypass RLS (if `SECURITY DEFINER` without checks)
- āŒ Execute arbitrary SQL (if poorly written)

## Risk Levels for Functions

| Type | Risk | Description |
|------|------|-------------|
| `SECURITY INVOKER` | Lower | Runs with caller's permissions |
| `SECURITY DEFINER` | Higher | Runs with definer's permissions |
| Accepts text/json | Higher | Potential for injection |
| Returns setof | Higher | Can return multiple rows |

## Usage

### Basic RPC Audit

```
Audit RPC functions on my Supabase project
```

### Test Specific Function

```
Test the get_user_data RPC function
```

## Output Format

```
═══════════════════════════════════════════════════════════
 RPC FUNCTIONS AUDIT
═══════════════════════════════════════════════════════════

 Project: abc123def.supabase.co
 Functions Found: 6

 ─────────────────────────────────────────────────────────
 Function Inventory
 ─────────────────────────────────────────────────────────

 1. get_user_profile(user_id uuid)
    Security: INVOKER
    Returns: json
    Status: āœ… SAFE

    Analysis:
    ā”œā”€ā”€ Uses auth.uid() for authorization
    ā”œā”€ā”€ Returns only caller's own profile
    └── RLS is respected

 2. search_posts(query text)
    Security: INVOKER
    Returns: setof posts
    Status: āœ… SAFE

    Analysis:
    ā”œā”€ā”€ Parameterized query (no injection)
    ā”œā”€ā”€ RLS filters results
    └── Only returns published posts

 3. get_all_users()
    Security: DEFINER
    Returns: setof users
    Status: šŸ”“ P0 - RLS BYPASS

    Analysis:
    ā”œā”€ā”€ SECURITY DEFINER runs as owner
    ā”œā”€ā”€ No auth.uid() check inside function
    ā”œā”€ā”€ Returns ALL users regardless of caller
    └── Bypasses RLS completely!

    Test Result:
    POST /rest/v1/rpc/get_all_users
    → Returns 1,247 user records with PII

    Immediate Fix:
    ```sql
    -- Add authorization check
    CREATE OR REPLACE FUNCTION get_all_users()
    RETURNS setof users
    LANGUAGE sql
    SECURITY INVOKER  -- Change to INVOKER
    AS $$
      SELECT * FROM users
      WHERE auth.uid() = id;  -- Add RLS-like check
    $$;
    ```

 4. admin_delete_user(target_id uuid)
    Security: DEFINER
    Returns: void
    Status: šŸ”“ P0 - CRITICAL VULNERABILITY

    Analysis:
    ā”œā”€ā”€ SECURITY DEFINER with delete capability
    ā”œā”€ā”€ No role check (anon can call!)
    ā”œā”€ā”€ Can delete any user
    └── No audit trail

    Test Result:
    POST /rest/v1/rpc/admin_delete_user
    Body: {"target_id": "any-uuid"}
    → Function accessible to anon!

    Immediate Fix:
    ```sql
    CREATE OR REPLACE FUNCTION admin_delete_user(target_id uuid)
    RETURNS void
    LANGUAGE plpgsql
    SECURITY DEFINER
    AS $$
    BEGIN
      -- Add role check
      IF NOT (SELECT is_admin FROM profiles WHERE id = auth.uid()) THEN
        RAISE EXCEPTION 'Unauthorized';
      END IF;

      DELETE FROM users WHERE id = target_id;
    END;
    $$;

    -- Or better: restrict to authenticated only
    REVOKE EXECUTE ON FUNCTION admin_delete_user FROM anon;
    ```

 5. dynamic_query(table_name text, conditions text)
    Security: DEFINER
    Returns: json
    Status: šŸ”“ P0 - SQL INJECTION

    Analysis:
    ā”œā”€ā”€ Accepts raw text parameters
    ā”œā”€ā”€ Likely concatenates into query
    ā”œā”€ā”€ SQL injection possible

    Test Result:
    POST /rest/v1/rpc/dynamic_query
    Body: {"table_name": "users; DROP TABLE users;--", "conditions": "1=1"}
    → Injection vector confirmed!

    Immediate Action:
    → DELETE THIS FUNCTION IMMEDIATELY

    ```sql
    DROP FUNCTION IF EXISTS dynamic_query;
    ```

    Never build queries from user input. Use parameterized queries.

 6. calculate_total(order_id uuid)
    Security: INVOKER
    Returns: numeric
    Status: āœ… SAFE

    Analysis:
    ā”œā”€ā”€ UUID parameter (type-safe)
    ā”œā”€ā”€ SECURITY INVOKER respects RLS
    └── Only accesses caller's orders

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

 Total Functions: 6
 Safe: 3
 P0 Critical: 3
   ā”œā”€ā”€ get_all_users (RLS bypass)
   ā”œā”€ā”€ admin_delete_user (no auth check)
   └── dynamic_query (SQL injection)

 Priority Actions:
 1. DELETE dynamic_query function immediately
 2. Add auth checks to admin_delete_user
 3. Fix get_all_users to respect RLS

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

## Injection Testing

The skill tests for SQL injection in text/varchar parameters:

### Safe (Parameterized)

```sql
-- āœ… Safe: uses parameter placeholder
CREATE FUNCTION search_posts(query text)
RETURNS setof posts
AS $$
  SELECT * FROM posts WHERE title ILIKE '%' || query || '%';
$$ LANGUAGE sql;
```

### Vulnerable (Concatenation)

```sql
-- āŒ Vulnerable: dynamic SQL execution
CREATE FUNCTION dynamic_query(tbl text, cond text)
RETURNS json
AS $$
DECLARE result json;
BEGIN
  EXECUTE format('SELECT json_agg(t) FROM %I t WHERE %s', tbl, cond)
  INTO result;
  RETURN result;
END;
$$ LANGUAGE plpgsql;
```

## Context Output

```json
{
  "rpc_audit": {
    "timestamp": "2025-01-31T11:00:00Z",
    "functions_found": 6,
    "summary": {
      "safe": 3,
      "p0_critical": 3,
      "p1_high": 0
    },
    "findings": [
      {
        "function": "get_all_users",
        "severity": "P0",
        "issue": "RLS bypass via SECURITY DEFINER",
        "impact": "All user data accessible",
        "remediation": "Change to SECURITY INVOKER or add auth checks"
      },
      {
        "function": "dynamic_query",
        "severity": "P0",
        "issue": "SQL injection vulnerability",
        "impact": "Arbitrary SQL execution possible",
        "remediation": "Delete function, use parameterized queries"
      }
    ]
  }
}
```

## Best Practices for RPC Functions

### 1. Prefer SECURITY INVOKER

```sql
CREATE FUNCTION my_function()
RETURNS ...
SECURITY INVOKER  -- Respects RLS
AS $$ ... $$;
```

### 2. Always Check auth.uid()

```sql
CREATE FUNCTION get_my_data()
RETURNS json
AS $$
  SELECT json_agg(d) FROM data d
  WHERE d.user_id = auth.uid();  -- Always filter by caller
$$ LANGUAGE sql SECURITY INVOKER;
```

### 3. Use REVOKE for Sensitive Functions

```sql
-- Remove anon access
REVOKE EXECUTE ON FUNCTION admin_function FROM anon;

-- Only authenticated users
GRANT EXECUTE ON FUNCTION admin_function TO authenticated;
```

### 4. Avoid Text Parameters for Dynamic Queries

```sql
-- āŒ Bad
CREATE FUNCTION query(tbl text) ...

-- āœ… Good: use specific functions per table
CREATE FUNCTION get_users() ...
CREATE FUNCTION get_posts() ...
```

## 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 function** → Log the action to `.sb-pentest-audit.log`
2. **After each function analyzed** → Immediately update `.sb-pentest-context.json`
3. **After each vulnerability found** → Log the finding 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. **Update `.sb-pentest-context.json`** with results:
   ```json
   {
     "rpc_audit": {
       "timestamp": "...",
       "functions_found": 6,
       "summary": { "safe": 3, "p0_critical": 3 },
       "findings": [ ... ]
     }
   }
   ```

2. **Log to `.sb-pentest-audit.log`**:
   ```
   [TIMESTAMP] [supabase-audit-rpc] [START] Auditing RPC functions
   [TIMESTAMP] [supabase-audit-rpc] [FINDING] P0: dynamic_query has SQL injection
   [TIMESTAMP] [supabase-audit-rpc] [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/rpc-tests/`

### Evidence Files to Create

| File | Content |
|------|---------|
| `function-list.json` | All discovered RPC functions |
| `vulnerable-functions/[name].json` | Details for each vulnerable function |

### Evidence Format (Vulnerable Function)

```json
{
  "evidence_id": "RPC-001",
  "timestamp": "2025-01-31T10:30:00Z",
  "category": "api-audit",
  "type": "rpc_vulnerability",
  "severity": "P0",

  "function": "get_all_users",

  "analysis": {
    "security_definer": true,
    "auth_check": false,
    "rls_bypass": true
  },

  "test": {
    "request": {
      "method": "POST",
      "url": "https://abc123def.supabase.co/rest/v1/rpc/get_all_users",
      "curl_command": "curl -X POST '$URL/rest/v1/rpc/get_all_users' -H 'apikey: $ANON_KEY' -H 'Content-Type: application/json'"
    },
    "response": {
      "status": 200,
      "rows_returned": 1247,
      "sample_data": "[REDACTED - contains user PII]"
    }
  },

  "impact": "Bypasses RLS, returns all 1,247 user records",
  "remediation": "Change to SECURITY INVOKER or add auth.uid() check"
}
```

### Add to curl-commands.sh

```bash
# === RPC FUNCTION TESTS ===
# Test get_all_users function (P0 if accessible)
curl -X POST "$SUPABASE_URL/rest/v1/rpc/get_all_users" \
  -H "apikey: $ANON_KEY" \
  -H "Content-Type: application/json"

# Test admin_delete_user function
curl -X POST "$SUPABASE_URL/rest/v1/rpc/admin_delete_user" \
  -H "apikey: $ANON_KEY" \
  -H "Content-Type: application/json" \
  -d '{"target_id": "test-uuid"}'
```

## Related Skills

- `supabase-audit-tables-list` — List exposed tables
- `supabase-audit-rls` — Test RLS policies
- `supabase-audit-auth-users` — User enumeration tests

Overview

This skill audits PostgreSQL RPC functions exposed through Supabase REST RPC endpoints. It discovers available functions, tests each for RLS bypass, SQL injection, and dangerous DEFINER behavior, and produces prioritized findings with recommended fixes. The skill is designed to save progress continuously so results survive interruptions.

How this skill works

The skill enumerates RPC functions via the Supabase project endpoint and inspects signatures, SECURITY DEFINER/INVOKER flags, parameter types, and return types. It actively tests functions by invoking them (with safe probes) to detect RLS bypass, excessive data exposure, and injection vectors, and it captures request/response evidence. Results and evidence are logged and written incrementally to persistent context and evidence files as each function is tested.

When to use it

  • During API or backend security assessments of Supabase projects
  • When verifying whether functions respect Row Level Security (RLS)
  • To identify SQL injection or dynamic SQL risks in RPC parameters
  • As part of a full pentest that includes table and auth audits
  • Before deploying new RPC functions to production

Best practices

  • Prefer SECURITY INVOKER for functions that should respect caller identity
  • Always filter results using auth.uid() or equivalent checks inside server-side code
  • Avoid text/varchar parameters that are used to build dynamic SQL; use typed parameters or explicit functions per resource
  • Revoke anon execute on admin-level functions and grant to authenticated or specific roles
  • Write progress to context and evidence files immediately before/after each test so findings are preserved if interrupted

Example use cases

  • List all RPC functions and produce an inventory with security classification
  • Test a specific function (e.g., get_all_users) for RLS bypass and capture sample responses
  • Detect SQL injection in dynamic query functions that accept text conditions
  • Produce prioritized remediation steps: delete dangerous functions, change to INVOKER, or add auth checks
  • Generate curl commands and evidence artifacts for inclusion in a pentest report

FAQ

Do I need project credentials to run this audit?

Yes. The Supabase URL and anon key (or service role key for deeper tests) are required to enumerate and invoke RPC endpoints.

What happens if the audit is interrupted?

The skill is built to write logs, context, and evidence progressively for each function so all completed findings are preserved even if execution stops.