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

This skill audits Supabase Realtime channels for unauthorized subscriptions and data exposure, logging findings progressively for secure channel configuration.

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

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

Files (1)
SKILL.md
14.1 KB
---
name: supabase-audit-realtime
description: Test Supabase Realtime WebSocket channels for unauthorized subscriptions and data exposure.
---

# Realtime Channel 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 channel tested**
> - Log to `.sb-pentest-audit.log` **BEFORE and AFTER each subscription 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 Supabase Realtime WebSocket channels for security issues.

## When to Use This Skill

- To check if Realtime channels are properly secured
- To detect unauthorized data streaming
- When Realtime is used for sensitive data
- As part of comprehensive security audit

## Prerequisites

- Supabase URL and anon key available
- Detection completed

## Understanding Supabase Realtime

Supabase Realtime enables:

```
wss://[project].supabase.co/realtime/v1/websocket
```

| Feature | Description |
|---------|-------------|
| Postgres Changes | Stream database changes |
| Broadcast | Pub/sub messaging |
| Presence | User presence tracking |

## Security Model

Realtime respects RLS policies:
- āœ… If RLS blocks SELECT, Realtime won't stream
- āŒ If RLS allows SELECT, Realtime streams data
- āš ļø Broadcast channels can be subscribed without RLS

## Tests Performed

| Test | Purpose |
|------|---------|
| Channel enumeration | Find open channels |
| Postgres Changes | Test table streaming |
| Broadcast | Test pub/sub access |
| Presence | Test presence channel access |

## Usage

### Basic Realtime Audit

```
Audit Realtime channels on my Supabase project
```

### Test Specific Feature

```
Test if Postgres Changes streams sensitive data
```

## Output Format

```
═══════════════════════════════════════════════════════════
 REALTIME CHANNEL AUDIT
═══════════════════════════════════════════════════════════

 Project: abc123def.supabase.co
 Endpoint: wss://abc123def.supabase.co/realtime/v1/websocket

 ─────────────────────────────────────────────────────────
 Connection Test
 ─────────────────────────────────────────────────────────

 WebSocket Connection: āœ… Established
 Authentication: Anon key accepted
 Protocol: Phoenix channels

 ─────────────────────────────────────────────────────────
 Postgres Changes Test
 ─────────────────────────────────────────────────────────

 Subscribing to table changes with anon key...

 Table: users
 ā”œā”€ā”€ Subscribe: āœ… Subscribed
 ā”œā”€ā”€ INSERT events: šŸ”“ P0 - RECEIVING ALL NEW USERS
 ā”œā”€ā”€ UPDATE events: šŸ”“ P0 - RECEIVING ALL UPDATES
 └── DELETE events: šŸ”“ P0 - RECEIVING ALL DELETES

 Sample Event Received:
 ```json
 {
   "type": "INSERT",
   "table": "users",
   "record": {
     "id": "550e8400-e29b-...",
     "email": "[email protected]",  ← PII STREAMING!
     "name": "New User",
     "created_at": "2025-01-31T10:00:00Z"
   }
 }
 ```

 Finding: šŸ”“ P0 - User data streaming without authentication!
          RLS may not be properly configured for Realtime.

 Table: orders
 ā”œā”€ā”€ Subscribe: āœ… Subscribed
 ā”œā”€ā”€ INSERT events: āŒ Not receiving (RLS working)
 ā”œā”€ā”€ UPDATE events: āŒ Not receiving (RLS working)
 └── DELETE events: āŒ Not receiving (RLS working)

 Assessment: āœ… Orders table properly protected.

 Table: posts
 ā”œā”€ā”€ Subscribe: āœ… Subscribed
 ā”œā”€ā”€ INSERT events: āœ… Receiving published only
 ā”œā”€ā”€ UPDATE events: āœ… Receiving published only
 └── DELETE events: āœ… Receiving published only

 Assessment: āœ… Posts streaming respects RLS (published only).

 ─────────────────────────────────────────────────────────
 Broadcast Channel Test
 ─────────────────────────────────────────────────────────

 Attempting to subscribe to common channel names...

 Channel: room:lobby
 ā”œā”€ā”€ Subscribe: āœ… Success
 ā”œā”€ā”€ Messages: Receiving broadcasts
 └── Assessment: ā„¹ļø Open channel (may be intentional)

 Channel: admin
 ā”œā”€ā”€ Subscribe: āœ… Success ← Should this be public?
 ā”œā”€ā”€ Messages: Receiving admin notifications
 └── Assessment: 🟠 P1 - Admin channel publicly accessible

 Channel: notifications
 ā”œā”€ā”€ Subscribe: āœ… Success
 ā”œā”€ā”€ Messages: Receiving user notifications for ALL users!
 └── Assessment: šŸ”“ P0 - User notifications exposed

 Sample Notification:
 ```json
 {
   "user_id": "123...",
   "type": "payment_received",
   "amount": 150.00,
   "from": "[email protected]"
 }
 ```

 ─────────────────────────────────────────────────────────
 Presence Test
 ─────────────────────────────────────────────────────────

 Channel: online-users
 ā”œā”€ā”€ Subscribe: āœ… Success
 ā”œā”€ā”€ Presence List: Receiving all online users
 └── Users Online: 47

 Sample Presence Data:
 ```json
 {
   "user_id": "550e8400-...",
   "email": "[email protected]",
   "status": "online",
   "last_seen": "2025-01-31T14:00:00Z"
 }
 ```

 Assessment: 🟠 P1 - User presence data exposed
             Consider if email/user_id should be visible.

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

 Postgres Changes:
 ā”œā”€ā”€ šŸ”“ P0: users table streaming all data
 ā”œā”€ā”€ āœ… PASS: orders table protected by RLS
 └── āœ… PASS: posts table correctly filtered

 Broadcast:
 ā”œā”€ā”€ šŸ”“ P0: notifications channel exposing user data
 ā”œā”€ā”€ 🟠 P1: admin channel publicly accessible
 └── ā„¹ļø INFO: lobby channel open (review if intended)

 Presence:
 └── 🟠 P1: online-users exposing user details

 Critical Findings: 2
 High Findings: 2

 ═══════════════════════════════════════════════════════════
 Recommendations
 ═══════════════════════════════════════════════════════════

 1. FIX USERS TABLE RLS
    Ensure RLS applies to Realtime:
    ```sql
    ALTER TABLE users ENABLE ROW LEVEL SECURITY;

    CREATE POLICY "Users see only themselves"
      ON users FOR SELECT
      USING (auth.uid() = id);
    ```

 2. SECURE BROADCAST CHANNELS
    Use Realtime Authorization:
    ```javascript
    // Require auth for sensitive channels
    const channel = supabase.channel('admin', {
      config: {
        broadcast: { ack: true },
        presence: { key: userId }
      }
    })

    // Server-side: validate channel access
    // Use RLS on realtime.channels table
    ```

 3. LIMIT PRESENCE DATA
    Only share necessary information:
    ```javascript
    channel.track({
      online_at: new Date().toISOString()
      // Don't include email, user_id unless needed
    })
    ```

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

## Realtime Security Model

### Postgres Changes + RLS

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

-- With this policy:
-- - API SELECT: Only own data
-- - Realtime: Only own data changes
```

### Broadcast Security

```sql
-- Realtime authorization (Supabase extension)
-- Add policies to realtime.channels virtual table

-- Only authenticated users can join
CREATE POLICY "Authenticated users join channels"
  ON realtime.channels FOR SELECT
  USING (auth.role() = 'authenticated');

-- Or restrict specific channels
CREATE POLICY "Admin channel for admins"
  ON realtime.channels FOR SELECT
  USING (
    name != 'admin' OR
    (SELECT is_admin FROM profiles WHERE id = auth.uid())
  );
```

## Context Output

```json
{
  "realtime_audit": {
    "timestamp": "2025-01-31T14:00:00Z",
    "connection": "established",
    "postgres_changes": {
      "users": {
        "subscribed": true,
        "receiving_events": true,
        "severity": "P0",
        "finding": "All user data streaming without RLS"
      },
      "orders": {
        "subscribed": true,
        "receiving_events": false,
        "severity": null,
        "finding": "Properly protected by RLS"
      }
    },
    "broadcast": {
      "notifications": {
        "accessible": true,
        "severity": "P0",
        "finding": "User notifications exposed"
      },
      "admin": {
        "accessible": true,
        "severity": "P1",
        "finding": "Admin channel publicly accessible"
      }
    },
    "presence": {
      "online-users": {
        "accessible": true,
        "severity": "P1",
        "users_visible": 47,
        "finding": "User presence data exposed"
      }
    }
  }
}
```

## Common Realtime Issues

| Issue | Cause | Fix |
|-------|-------|-----|
| All data streaming | RLS not enabled/configured | Enable and configure RLS |
| Broadcast open | No channel authorization | Add channel policies |
| Presence exposed | Too much data tracked | Minimize tracked data |

## Remediation Examples

### Secure Table Streaming

```sql
-- Ensure RLS is enabled
ALTER TABLE users ENABLE ROW LEVEL SECURITY;

-- Policy for authenticated users only
CREATE POLICY "Users see own profile" ON users
  FOR SELECT
  USING (auth.uid() = id);

-- Realtime will now only stream changes for the authenticated user's row
```

### Secure Broadcast Channels

```javascript
// Client: Check access before subscribing
const { data: canAccess } = await supabase
  .from('channel_access')
  .select('*')
  .eq('channel', 'admin')
  .eq('user_id', userId)
  .single();

if (canAccess) {
  const channel = supabase.channel('admin');
  channel.subscribe();
}
```

### Minimal Presence Data

```javascript
// Before (too much data)
channel.track({
  user_id: userId,
  email: email,
  name: fullName,
  avatar: avatarUrl
});

// After (minimal data)
channel.track({
  online_at: new Date().toISOString()
  // User details fetched separately if needed
});
```

## 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 channel** → Log the action to `.sb-pentest-audit.log`
2. **After each data exposure found** → Immediately update `.sb-pentest-context.json`
3. **After each subscription test** → Log the result 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
   {
     "realtime_audit": {
       "timestamp": "...",
       "connection": "established",
       "postgres_changes": { ... },
       "broadcast": { ... },
       "presence": { ... }
     }
   }
   ```

2. **Log to `.sb-pentest-audit.log`**:
   ```
   [TIMESTAMP] [supabase-audit-realtime] [START] Auditing Realtime channels
   [TIMESTAMP] [supabase-audit-realtime] [FINDING] P0: users table streaming all data
   [TIMESTAMP] [supabase-audit-realtime] [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/06-realtime-audit/`

### Evidence Files to Create

| File | Content |
|------|---------|
| `websocket-connection.json` | WebSocket connection test |
| `postgres-changes/[table].json` | Table subscription results |
| `broadcast-channels/[channel].json` | Broadcast channel access |
| `presence-data/[channel].json` | Presence data exposure |

### Evidence Format

```json
{
  "evidence_id": "RT-001",
  "timestamp": "2025-01-31T11:05:00Z",
  "category": "realtime-audit",
  "type": "postgres_changes",
  "severity": "P0",

  "table": "users",

  "subscription_test": {
    "channel": "realtime:public:users",
    "subscribed": true,
    "events_received": true
  },

  "sample_event": {
    "type": "INSERT",
    "table": "users",
    "record": {
      "id": "[REDACTED]",
      "email": "[REDACTED]@example.com",
      "name": "[REDACTED]"
    },
    "redacted": true
  },

  "impact": {
    "pii_streaming": true,
    "affected_columns": ["email", "name"],
    "rls_bypass": true
  },

  "websocket_url": "wss://abc123def.supabase.co/realtime/v1/websocket",

  "reproduction_code": "const channel = supabase.channel('realtime:public:users').on('postgres_changes', { event: '*', schema: 'public', table: 'users' }, (payload) => console.log(payload)).subscribe()"
}
```

## Related Skills

- `supabase-audit-rls` — RLS affects Realtime
- `supabase-audit-tables-read` — API access is related
- `supabase-report` — Include in final report

Overview

This skill tests Supabase Realtime WebSocket channels for unauthorized subscriptions and sensitive data exposure. It enumerates channels, attempts table streaming, broadcast subscriptions, and presence checks to identify RLS bypasses and open broadcast channels. Findings are saved progressively and packaged with evidence for remediation.

How this skill works

The skill connects to the Supabase Realtime WebSocket and enumerates common channel names and table change topics. For each subscription it logs the attempt, records events (INSERT/UPDATE/DELETE), and evaluates whether RLS or channel authorization prevents data streaming. Results and evidence files are written immediately after each test to preserve progress.

When to use it

  • When Realtime is enabled on a Supabase project
  • During security audits of apps using live updates or presence features
  • To verify that RLS policies apply to realtime streams
  • Before deploying features that broadcast user-sensitive data
  • To detect open broadcast or presence channels exposing PII

Best practices

  • Always enable and test Row Level Security for tables that contain PII
  • Treat broadcast channels as potentially public unless explicitly restricted by channel policies
  • Limit tracked presence fields to non-identifying values (timestamps, status)
  • Write context and evidence files immediately after each channel test to avoid data loss
  • Redact PII from stored evidence and include reproduction steps instead of raw user data

Example use cases

  • Detecting that the users table streams full records over realtime despite RLS not being enforced
  • Finding an admin or notifications broadcast channel that is publicly subscribable
  • Verifying that orders or payments tables are protected and do not stream events to anon keys
  • Auditing presence channels to ensure only minimal, non-PII data is tracked
  • Generating progressive audit logs and per-channel evidence for a consolidated report

FAQ

What credentials are required to run this audit?

You need the Supabase project URL and a valid anon key for initial connectivity; tests assume detection of project endpoints has already completed.

How are findings preserved if the audit is interrupted?

The skill writes a log entry before each subscription test and updates the context and evidence files immediately after each test so all prior findings are preserved even on crash.