plan-rls-audit — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited plan-rls-audit (Agent Skill) and scored it 45/100 (orange). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 1 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 1 flagged
A base64 string of 128+ characters appears in a documentation file. Encoded prompt injection hides the hostile instruction in base64 — invisible to keyword filters — and relies on the agent's ability to decode it at runtime. There is no normal authoring reason to embed a multi-hundred-byte base64 blob in skill docs.
*.sig, SIGNATURES) outside the documentation.Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
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. Moltbook leaked 1.5M API keys and 35k emails from exactly this. CVE-2025-48757 covered 170 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.
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.
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.
Walk the schema (via Supabase MCP, pg_tables, migration files, or the dashboard security advisor). For every table and view:
relrowsecurity = true for 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.
security_invoker vs definer)."main" tables are protected.
WITH CHECK (true)) on anything user-owned = the tableis public. The agent writes this to make a "permission denied" error go away; it's the single most common dangerous pattern.
checks auth but blocks the wrong party. Read each policy's truth table, don't trust its name.
policy (or vice-versa) — RLS denies by default only if RLS is on, so confirm each operation the app performs is actually governed.
auth.uid() but doesn't tie it toa row-owner column (user_id = auth.uid()), so any logged-in user reads every other user's rows.
entirely and must never be shipped client-side or in NEXT_PUBLIC_*. If found: Critical, rotate immediately (hand to plan-secrets-audit).
anon access is safe because RLS holds — not by accident.
service_role: verify they re-checkauthorization in code, since they've opted out of RLS.
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).
auth.jwt() / custom claims usage for the same per-row trap.pg_policies cross-checked against the app's actual access matrix — flag anytable 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).
relrowsecurity state and policy set. State how you accessed it (MCP / migrations / advisor) and any tables you couldn't see.
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).
the deliverable's most useful artifact.
High, then Med/Low. Each phase maps to an execution skill.
ALTER TABLE, no CREATE POLICY, no migrations. Thedeliverable is the report and the access matrix.
loosening a policy to test something. Read state; don't mutate it.
job is to confirm RLS makes that safe, not to treat the key as a secret.
if the fix is one line. Rank by what's reachable today.
(.eq('user_id', currentUser) in the client query) is not RLS; flag it as unprotected — anyone can drop the filter in dev tools.
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.plan-input-validation; credentials:plan-secrets-audit; data access: this skill; blast radius: plan-data-integrity; observability: plan-error-handling.
service_role key found here handsstraight over (rotation, not relocation).
db-migrator, backend-patterns, backend-db-performance,audit-security.
plan-rls-audit pass.Plan with a strong model; execute with composer-2.5-execution.mdc riding along. The plan says which tables are exposed; the rule constrains how the migration is allowed to touch them.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.