Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions skills/.experimental/supabase-security-audit/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
---
name: supabase-security-audit
description: Audit and harden Supabase or PostgreSQL projects by reviewing database schema, row level security coverage, policy correctness, service-role exposure, auth boundaries, and common application security mistakes. Use when Codex needs to add or fix RLS on existing tables, inspect Supabase migrations, review server and client auth code, or investigate unknown security vulnerabilities in a Supabase-backed app.
---

# Supabase Security Audit

## Overview

Audit Supabase and PostgreSQL projects for authorization gaps and common security mistakes. Prefer concrete findings and code or SQL fixes over generic advice.

## Quick Start

1. Read the migration files under `supabase/`, `db/`, or other SQL directories.
2. Read the Supabase client wrappers, auth helpers, API routes, server actions, storage handlers, and env loaders.
3. Run `python3 scripts/audit_supabase_security.py <project-root>` from this skill directory when a static scan will save time.
4. Read `references/audit-checklist.md` for the full review sequence.
5. Read `references/rls-policy-patterns.md` when writing or tightening policies.

## Workflow

### 1. Inventory the trust boundaries

List:

- public tables and views
- user-owned tables
- backend-only tables
- service-role code paths
- client code that talks to Supabase directly
- privileged functions, triggers, workers, and webhooks

Treat every `public` table as user-reachable until proven otherwise.

### 2. Check RLS coverage first

For each application table:

- Ensure `alter table ... enable row level security` exists.
- Prefer `force row level security` when owners should still be bound by policies.
- Treat `RLS enabled but no policies` as deny-all. Accept that state only when the table is intentionally backend-only.
- Flag `using (true)` and `with check (true)` for review instead of assuming they are safe.

If a table has no RLS, add it before doing anything else.

### 3. Write least-privilege policies

Choose the smallest valid audience:

- owner only
- admin only
- public read with scoped write
- join-based access through a parent ownership table

Avoid blanket `for all` policies unless the same rule is correct for every command. Prefer separate `select`, `insert`, `update`, and `delete` policies when rules differ.

### 4. Review privileged SQL

Inspect:

- `security definer` functions
- triggers that write rows on behalf of users
- grants to `anon` or `authenticated`
- views or functions that can bypass intended RLS behavior
- migrations that backfill data and forget to restore protections

Require a concrete reason for every privileged object. When a `security definer` function is necessary, keep it schema-qualified and set an explicit `search_path`.

### 5. Review application-side security

Check for:

- service-role secrets in client bundles or `NEXT_PUBLIC_*` variables
- API routes and server actions that trust user input without ownership checks
- storage upload or signing endpoints that let one user act on another user's files
- admin-only flows guarded only in the UI
- worker or webhook code that writes to tables whose policies assume end-user auth

Keep server-side authorization checks even when RLS already exists.

### 6. Apply fixes and verify

When hardening the project:

- create a new migration instead of rewriting history unless the repo clearly treats the schema as disposable
- enable RLS on uncovered tables
- add or tighten policies
- remove or narrow risky grants and helper functions
- verify that allowed actors still succeed and disallowed actors fail

## Output

Report findings in severity order with:

- object or file
- impact
- exact fix
- follow-up verification

When asked to implement hardening, summarize which actor can now access each protected table or endpoint.

## Resources

- `scripts/audit_supabase_security.py`: Static scanner for SQL RLS coverage, risky policies, privileged functions, and basic code-side exposure checks.
- `references/audit-checklist.md`: Full review checklist and live-database SQL queries.
- `references/rls-policy-patterns.md`: Reusable policy patterns for owner, admin, public, and backend-only tables.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
interface:
display_name: "Supabase Security Audit"
short_description: "Audit Supabase security, RLS, and auth boundaries"
default_prompt: "Use $supabase-security-audit to review database RLS, auth boundaries, and security risks in this project."
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Audit Checklist

Use this checklist when the task is a real security review rather than a quick RLS fix.

## 1. Schema inventory

- List every application table, view, function, trigger, bucket, worker, and webhook.
- Mark each table as `user-facing`, `shared/public-read`, `admin-only`, or `backend-only`.
- Treat anything in the `public` schema as exposed to authenticated users unless proven otherwise.

## 2. RLS and policy review

- Confirm `enable row level security` on every application table.
- Decide whether `force row level security` is needed.
- Review each policy by command, not only by table.
- Check `using` and `with check` separately.
- Flag `for all`, `using (true)`, and `with check (true)` for manual review.
- Confirm child-table access is constrained through the owning parent row.

## 3. Privileged SQL review

- Review every `security definer` function.
- Require an explicit `search_path` on privileged functions.
- Review custom grants to `anon`, `authenticated`, and `public`.
- Check triggers that insert rows into protected tables.
- Verify helper functions do not create policy recursion against RLS-protected tables.

## 4. App and API review

- Confirm service-role keys stay on the server only.
- Check `NEXT_PUBLIC_*` env vars for accidental secret exposure.
- Verify API routes and server actions enforce ownership before writes.
- Check uploads, download signing, and storage paths for cross-tenant access.
- Confirm admin flows are enforced on the server, not only hidden in the UI.
- Review webhook handlers and workers that bypass user-context RLS.

## 5. Verification

- Test one allowed actor and one disallowed actor for each sensitive table or endpoint.
- Confirm public pages only read rows intended for public access.
- Confirm owners cannot mutate another owner's rows.
- Confirm backend-only tables deny normal user tokens.

## Live SQL Queries

### Tables and RLS state

```sql
select
n.nspname as schema_name,
c.relname as table_name,
c.relrowsecurity as rls_enabled,
c.relforcerowsecurity as rls_forced
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where c.relkind = 'r'
and n.nspname not in ('pg_catalog', 'information_schema')
order by 1, 2;
```

### Policies

```sql
select
schemaname,
tablename,
policyname,
cmd,
roles,
qual,
with_check
from pg_policies
order by 1, 2, 3;
```

### Security definer functions

```sql
select
n.nspname as schema_name,
p.proname as function_name,
p.prosecdef as security_definer
from pg_proc p
join pg_namespace n on n.oid = p.pronamespace
where n.nspname not in ('pg_catalog', 'information_schema')
and p.prosecdef
order by 1, 2;
```

## Triage

- `High`: public write access, cross-tenant reads, client-exposed secrets, or admin bypass.
- `Medium`: missing RLS, broad policies without proof they are intentional, unsafe privileged helpers.
- `Low`: missing hardening, incomplete verification, or hygiene issues that do not create direct access today.
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# RLS Policy Patterns

Use these patterns as starting points. Adapt column names and helper functions to the project.

## Owner can read and update their own row

```sql
alter table public.profiles enable row level security;

create policy "profiles self read"
on public.profiles
for select
using (id = auth.uid());

create policy "profiles self update"
on public.profiles
for update
using (id = auth.uid())
with check (id = auth.uid());
```

## Owner inserts child rows

```sql
create policy "orders buyer insert"
on public.orders
for insert
with check (buyer_id = auth.uid());
```

Match the inserted owner column in `with check`. Do not rely on a UI-hidden field.

## Child table inherits access from parent ownership

```sql
create policy "assets owner read"
on public.product_assets
for select
using (
exists (
select 1
from public.products p
where p.id = product_id
and p.seller_id = auth.uid()
)
);
```

Use `exists` against the owning table when the child row has no direct `user_id`.

## Public read with owner or admin override

```sql
create policy "products public approved read"
on public.products
for select
using (
status = 'approved'
or seller_id = auth.uid()
or public.is_admin()
);
```

Reserve public read for explicitly public rows. Keep write policies separate.

## Admin-only table

```sql
alter table public.admin_actions enable row level security;

create policy "admin actions admin read"
on public.admin_actions
for select
using (public.is_admin());
```

Prefer explicit admin-only policies over application-only checks.

## Backend-only table

```sql
alter table public.internal_jobs enable row level security;
```

Leave the table with no user-facing policies when only service-role code should access it. Document why the deny-all state is intentional.

## Security definer helper notes

- Use `security definer` only when ordinary RLS-aware SQL cannot express the requirement.
- Set `search_path` explicitly on privileged functions.
- Keep helper functions schema-qualified.
- Avoid helper functions that query the same RLS-protected table used in their calling policy unless the recursion behavior is proven safe.

## Anti-patterns

- `with check (true)` on user-driven inserts or updates.
- `using (true)` on update or delete policies.
- one `for all` policy when read and write rules differ.
- trusting a service-role API route without server-side ownership checks.
- exposing secrets through `NEXT_PUBLIC_*`.
Loading