Plan rls audit
π¦Curated Cursor AI agent skills, slash commands, MCP configs, subagents & rules for full-stack dev β React 19, Next.js 15, Supabase, Tailwind v4, TypeScript
npx -y skills add kensaurus/cursor-kenji --skill plan-rls-auditAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 6 stars6 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Audit a Supabase/Postgres project for Row-Level Security and access-control gaps, then produce a phased remediation plan. Use when the user says "RLS", "is my Supabase secure", "anyone can read my data", "lock down my tables", "service_role key", or is hardening before launch. Audits every table for relrowsecurity, permissive/inverted policies, client-side service_role keys, and auth.uid() correctness. Plan only β nothing altered until approved. Pairs with plan-secrets-audit, plan-security-audit, audit-db-schema. Do NOT use for query performance (backend-db-performance) or non-Postgres backends.
The file declares its own license as MIT. That is the authorβs claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
9.3 KB, ~2.2k tokens by cl100k_base, as published. Nobody here has run it
RLS & Access-Control Audit + Remediation Plan
Role: Senior backend engineer + Supabase security specialist.
Task: Enumerate every table/view, run the RLS checklist (AβE), build a who-can-do-what
access matrix, score findings by exposure, emit plan-rls-audit.md. Audit & plan only β
no SQL runs until each phase is approved.
Missing or misconfigured Row-Level Security is the most documented vibe-coding catastrophe of the 2025β2026 era. An audit of 50 vibe-coded apps across Lovable, Bolt, v0, Cursor, and Claude Code found 88% had RLS entirely disabled β not misconfigured, disabled β meaning the database returned any row to any query. Public breach reports describe apps leaking millions of API keys and user emails from exactly this, and a disclosed vulnerability class spanning many production apps where access control was present but logically inverted β authenticated users blocked, anonymous users granted everything.
This skill is the audit-and-plan half of fixing that. Execution is handed to
backend-patterns / db-migrator / audit-security after you approve each phase.
When this fires
Trigger phrases: "check my RLS", "is my Supabase locked down", "can anyone read my tables", "audit my database policies", "service_role key", "Supabase security advisor flagged something", "pre-launch DB hardening".
Do not fire for: pure query speed (backend-db-performance), schema/naming
design (audit-db-schema), or broad app-layer OWASP (plan-security-audit).
RLS is specifically who can read/write which rows, enforced at the database.
Why a dedicated skill (not generic security audit)
RLS failure is mechanical and table-by-table. A generic OWASP sweep glances at it; this enumerates every table and applies a deterministic checklist. Fixing RLS is the first step when the anon key can read rows it should not.
The audit checklist
Walk the schema (via Supabase MCP, pg_tables, migration files, or the dashboard
security advisor). For every table and view:
A Β· Is RLS even on?
- Check
relrowsecurity = truefor every table in exposed schemas (publicespecially). Any table with RLS off is fully readable/writable through the anon key. This is finding #1, severity Critical, every time it appears. - Views: confirm they don't silently bypass RLS (
security_invokervs definer). - Don't forget junction/lookup tables β they leak relationships even when the "main" tables are protected.
B Β· Are the policies real, or theater?
USING (true)(orWITH CHECK (true)) on anything user-owned = the table is public. The agent writes this to make a "permission denied" error go away; it's the single most common dangerous pattern.- Inverted logic (the disclosed inverted-policy class): a policy that looks like it checks auth but blocks the wrong party. Read each policy's truth table, don't trust its name.
- Missing operation coverage: a SELECT policy with no INSERT/UPDATE/DELETE policy (or vice-versa) β RLS denies by default only if RLS is on, so confirm each operation the app performs is actually governed.
- Ownership not enforced: policy references
auth.uid()but doesn't tie it to a row-owner column (user_id = auth.uid()), so any logged-in user reads every other user's rows.
C Β· Key scoping (the client-side trap)
- Search frontend/client bundles for the
service_rolekey. It bypasses RLS entirely and must never be shipped client-side or inNEXT_PUBLIC_*. If found: Critical, rotate immediately (hand toplan-secrets-audit). - Confirm the anon key is the only Supabase key in client code, and that anon access is safe because RLS holds β not by accident.
- Edge Functions / server routes using
service_role: verify they re-check authorization in code, since they've opted out of RLS.
D Β· Correctness & performance of auth calls
- Supabase's own linter flags
auth.uid()called per-row. Recommend(select auth.uid())so Postgres caches it β correctness is unchanged but large tables stop timing out (a real fix, not cosmetic). - Check
auth.jwt()/ custom claims usage for the same per-row trap.
E Β· Defaults & adjacent gates
- Storage buckets: public vs RLS-governed; signed-URL usage.
- Realtime: row-level authorization on subscribed channels.
pg_policiescross-checked against the app's actual access matrix β flag any table the app reads/writes that has no corresponding policy.
For each finding: table/policy, the exact gap, who can currently access what, severity, and a remediation direction (not the SQL β that's execution).
Procedure
- Inventory. List every table/view in exposed schemas with its
relrowsecuritystate and policy set. State how you accessed it (MCP / migrations / advisor) and any tables you couldn't see. - Classify. Run each through checklist AβE. Tag severity:
Critical (RLS off,
USING(true), service_role client-side, inverted policy), High (ownership not enforced, missing operation coverage), Med (perf-trap auth calls, view bypass), Low (hardening nits). - Build the access matrix. A who-can-do-what table the user can eyeball β the deliverable's most useful artifact.
- Phase the burndown. Critical first (data is exposed right now), then High, then Med/Low. Each phase maps to an execution skill.
- Emit
plan-rls-audit.md. End the turn. Do not write or run any SQL.
Guardrails
- Plan only. No
ALTER TABLE, noCREATE POLICY, no migrations. The deliverable is the report and the access matrix. - Never widen access to "verify". Don't suggest temporarily disabling RLS or loosening a policy to test something. Read state; don't mutate it.
- Assume the anon key is public. It is β it ships in the browser. The plan's job is to confirm RLS makes that safe, not to treat the key as a secret.
- Severity is about exposure, not effort. An RLS-off table is Critical even if the fix is one line. Rank by what's reachable today.
- Don't confuse "works in the demo" with "secure". App-layer filtering
(
.eq('user_id', currentUser)in the client query) is not RLS; flag it as unprotected β anyone can drop the filter in dev tools. - Minimal quoting of policy bodies; identify by table + operation.
Report template β plan-rls-audit.md
# RLS & Access-Control Audit β <project>
_Audit-only. No SQL runs until each phase is approved._
## Scope
- Tables/views audited: n | Source: [Supabase MCP / migrations / advisor]
- Couldn't inspect: β¦ | Assumptions: β¦
## Verdict
| Severity | Count | Worst case if shipped |
|----------|-------|-----------------------|
| Critical | n | full DB readable via anon key |
| High | n | cross-user data access |
| Medium | n | timeouts / view bypass |
| Low | n | hardening |
## Access matrix (current reality)
| Table | RLS on? | anon can | authed (own) | authed (others') | service_role exposed? |
|-------|---------|----------|--------------|------------------|-----------------------|
| profiles | β | read+write all | β | YES β | β |
## Findings
| # | Table / policy | Gap | Who's exposed | Sev | Direction |
|---|----------------|-----|---------------|-----|-----------|
| R1 | profiles | RLS disabled | everyone, anon | Crit | enable RLS; add owner policy |
| R2 | orders | USING (true) on SELECT | every authed user reads all orders | Crit | scope to user_id = (select auth.uid()) |
## Phased burndown
- **Phase 1 β Stop active exposure** β `db-migrator` / `backend-patterns` β R1, R2β¦
- **Phase 2 β Enforce ownership** β `db-migrator` β H-tier items
- **Phase 3 β Correctness & perf** β `backend-db-performance` β auth.uid() traps
- **Phase 4 β Adjacent gates** β `audit-security` β storage, realtime
## Execution handoff
Approve a phase to run it. Re-run `plan-rls-audit` after to confirm closure, and
cross-check with Supabase's security advisor.
Chains with
- Security spine β entry:
plan-input-validation; credentials:plan-secrets-audit; data access: this skill; blast radius:plan-data-integrity; observability:plan-error-handling. plan-secrets-auditβ any exposedservice_rolekey found here hands straight over (rotation, not relocation).- Execution:
db-migrator,backend-patterns,backend-db-performance,audit-security. - Verify: Supabase security advisor + a second
plan-rls-auditpass.
Plan with a strong model; execute with
composer-2.5-execution.mdcriding along. The plan says which tables are exposed; the rule constrains how the migration is allowed to touch them.
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.