{"manifest":{"name":"Supabase RLS Auditor","version":"1.0.0","description":"Audit Supabase Row Level Security policies for correctness and security: checks for missing RLS, unrestricted USING (true), missing WITH CHECK, anon access leaks, service-role bypasses, and auth.uid() typos that silently lock out all users.","tags":["supabase","database","security","rls","postgresql","audit"],"standard":"agentskills.io","standard_version":"1.0","content_checksum":"8dd1877257678142ed3cf23b7a5bfea2a29239c2738495505df1ce81ea3bc2d8","bundle_checksum":null,"metadata":{},"files":[]},"files":{"SKILL.md":"# Supabase RLS Auditor\n\n> **Purpose:** Audit a Supabase database migration file or SQL containing Row Level Security (RLS) policies for correctness and security. Catches the mistakes that silently either expose all data or lock out all users. Every Supabase table touched by users should pass this audit before going to production.\n\n---\n\n## Invocation\n\n```\n/rls-audit <migration-file-or-sql>\n```\n\n---\n\n## The 8-Point RLS Checklist\n\n### Check 1: RLS Enabled on Every Table\n\nEvery table that stores user data must have:\n```sql\nALTER TABLE [table_name] ENABLE ROW LEVEL SECURITY;\n```\n\n**Failure:** Table has no RLS — all authenticated users can read/write all rows.\n\n---\n\n### Check 2: No Unrestricted `USING (true)` on Sensitive Tables\n\n```sql\n-- ❌ BAD — exposes ALL rows to ALL authenticated users\nCREATE POLICY \"all_read\" ON users FOR SELECT USING (true);\n\n-- ✅ GOOD — only user's own rows\nCREATE POLICY \"own_rows\" ON users FOR SELECT USING (auth.uid() = user_id);\n```\n\n**Exception:** Public tables (e.g., `skills` with `status = 'active'`) may use `USING (true)` for SELECT only.\n\n---\n\n### Check 3: INSERT Policies Have `WITH CHECK`\n\n```sql\n-- ❌ BAD — INSERT policy with no WITH CHECK\nCREATE POLICY \"insert_tip\" ON tips FOR INSERT TO authenticated USING (true);\n\n-- ✅ GOOD\nCREATE POLICY \"insert_tip\" ON tips FOR INSERT TO authenticated\n  WITH CHECK (auth.uid() = tipper_id);\n```\n\n---\n\n### Check 4: No Anon Role Access to Private Tables\n\nPolicies with `TO anon` on tables containing user data are dangerous. Flag any policy where:\n- Role is `anon` or omitted (defaults to all roles including anon)\n- Table contains PII (email, phone, address) or financial data\n\n---\n\n### Check 5: Service Role Bypasses Are Documented\n\n```sql\n-- This bypasses ALL RLS — it should be intentional and documented\nCREATE POLICY \"service_role_all\" ON [table]\n  USING (auth.role() = 'service_role');\n```\n\nFlag: every service-role bypass needs a comment explaining WHY it's needed.\n\n---\n\n### Check 6: auth.uid() Typo Check\n\nCommon typo that silently evaluates to `NULL = user_id` (always false — locks out all users):\n```sql\n-- ❌ BAD TYPOS\nUSING (auth.uid = user_id)         -- missing () → NULL\nUSING (auth.uid() = userId)        -- camelCase column name\nUSING (auth_uid() = user_id)       -- underscore instead of dot\n\n-- ✅ CORRECT\nUSING (auth.uid() = user_id)\n```\n\n---\n\n### Check 7: Separate Policies per Operation\n\n```sql\n-- ❌ BAD — FOR ALL lets users UPDATE and DELETE other users' data if USING passes\nCREATE POLICY \"user_access\" ON orders FOR ALL USING (auth.uid() = user_id);\n\n-- ✅ GOOD — explicit per-operation policies\nCREATE POLICY \"select_own\" ON orders FOR SELECT USING (auth.uid() = user_id);\nCREATE POLICY \"insert_own\" ON orders FOR INSERT WITH CHECK (auth.uid() = user_id);\nCREATE POLICY \"update_own\" ON orders FOR UPDATE USING (auth.uid() = user_id)\n  WITH CHECK (auth.uid() = user_id);\n```\n\n---\n\n### Check 8: Recursive Policy Risk\n\nPolicies that query the same table they protect can cause infinite recursion. Flag any policy that contains a subquery on the same table.\n\n---\n\n## Output Format\n\n```\n## RLS Audit: [migration file name]\n\n### Tables Audited: N\n\n---\n\n#### [table_name]\n\n| Check | Status | Notes |\n|-------|--------|-------|\n| RLS enabled | ✅ / ❌ FAIL | |\n| No unrestricted USING | ✅ / ⚠️ REVIEW | |\n| INSERT has WITH CHECK | ✅ / ❌ FAIL | |\n| No anon leaks | ✅ / ❌ FAIL | |\n| Service bypasses documented | ✅ / ❌ FAIL | |\n| auth.uid() correct | ✅ / ❌ FAIL | |\n| Per-operation policies | ✅ / ⚠️ REVIEW | |\n| No recursion risk | ✅ / ❌ FAIL | |\n\n**[table_name] verdict:** PASS / FAIL / NEEDS REVIEW\n\n---\n\n### Overall Audit Result: PASS / FAIL\n\nCritical issues (must fix before production): [N]\nWarnings (review before production): [N]\n\n### Fix SQL\n\n[SQL to fix any FAIL items]\n```\n\n---\n\n## Rules\n\n- Check 1 (RLS enabled) failure is always CRITICAL — no exceptions\n- Check 2 (USING true) on private tables is always CRITICAL\n- Flag any policy targeting `FOR ALL` — it's almost always wrong\n- If a table has no policies at all (but RLS is enabled): that's a lockout — all inserts/selects will fail silently\n\n## Playground\n\n<!DOCTYPE html><html><head><meta charset='utf-8'><style>*{box-sizing:border-box;margin:0;padding:0}body{background:#0d1117;color:#e6edf3;font-family:monospace;font-size:12px;height:100vh;display:flex;flex-direction:column;overflow:hidden}.header{background:#161b22;border-bottom:1px solid #30363d;padding:8px 14px;font-size:11px;color:#8b949e;display:flex;justify-content:space-between;align-items:center;flex-shrink:0}.title{color:#58a6ff;font-weight:bold;font-size:13px}.panels{display:flex;flex:1;overflow:hidden}.panel{flex:1;overflow:auto;padding:12px;border-right:1px solid #30363d}.panel:last-child{border-right:none}.label{font-size:10px;color:#8b949e;text-transform:uppercase;letter-spacing:.08em;margin-bottom:6px}pre{white-space:pre-wrap;word-break:break-word;line-height:1.5}</style></head><body><div class='header'><span class='title'>Supabase RLS Auditor</span><span>Example · SkillSlap</span></div><div class='panels'><div class='panel'><div class='label'>Input: RLS policy</div><pre><span style='color:#8b949e'>-- skills table</span>\n<span style='color:#8b949e'>CREATE POLICY \"read_public\"</span>\n<span style='color:#8b949e'>  ON skills FOR SELECT</span>\n<span style='color:#8b949e'>  USING (true);</span>\n\n<span style='color:#8b949e'>CREATE POLICY \"author_update\"</span>\n<span style='color:#8b949e'>  ON skills FOR UPDATE</span>\n<span style='color:#8b949e'>  USING (author_id = auth.uid());</span></pre></div><div class='panel'><div class='label'>Output: Audit findings</div><pre><span style='color:#f85149'>🔴 read_public: USING(true)</span>\n<span style='color:#f85149'>Returns ALL rows including unlisted</span>\n<span style='color:#f85149'>and draft skills to any anon user.</span>\n<span style='color:#f85149'>Fix: USING(visibility = 'public')</span>\n\n<span style='color:#3fb950'>✅ author_update: correct</span>\n<span style='color:#8b949e'>Only skill author can UPDATE. Good.</span>\n\n<span style='color:#e3b341'>🟡 Missing: DELETE policy</span>\n<span style='color:#e3b341'>Explicit policy documents intent:</span>\n<span style='color:#e3b341'>CREATE POLICY \"author_delete\"</span>\n<span style='color:#e3b341'>  ON skills FOR DELETE</span>\n<span style='color:#e3b341'>  USING (author_id = auth.uid());</span>\n\n<span style='color:#e3b341'>🟡 Missing: INSERT policy for authors</span></pre></div></div></body></html>"}}