Skip to content
SlashClone
Made by@shipcheck

Supabase rls audit

@shipcheck/supabase-rls-audit

Audit row level security in your Supabase migrations before you launch. Finds tables with RLS off, policies that let anyone write, policies that trust user_metadata, views that skip RLS, and risky SECURITY DEFINER functions, then drafts a fix migration for you to review. Reads .sql files only and never touches your database.

SkillDevOpsv1.0.0 (latest)Tested
Activity
New: be the first to try it
  1. 1{
  2. 2 "name": "supabase-rls-audit",
  3. 3 "owner": "shipcheck",
  4. 4 "version": "1.0.0",
  5. 5 "type": "skill",
  6. 6 "description": "Audit row level security in your Supabase migrations before you launch. Finds tables with RLS off, policies that let anyone write, policies that trust user_metadata, views that skip RLS, and risky SECURITY DEFINER functions, then drafts a fix migration for you to review. Reads .sql files only and never touches your database.",
  7. 7 "category": "devops",
  8. 8 "license": "MIT",
  9. 9 "compat": ["claude-code"],
  10. 10 "permissions": {
  11. 11 "shell": ["node scripts/rls-audit.mjs"],
  12. 12 "files": {
  13. 13 "read": ["./supabase/migrations/**", "./supabase/schema.sql"],
  14. 14 "write": ["./out/**", "./supabase/migrations/**"]
  15. 15 }
  16. 16 },
  17. 17 "tests": {
  18. 18 "cases": [
  19. 19 {
  20. 20 "name": "flags a table with RLS off and a policy that lets any user write",
  21. 21 "inputs": {
  22. 22 "supabase/migrations/0001_init.sql": "create table public.todos (\n id bigint generated always as identity primary key,\n user_id uuid not null,\n title text\n);\nalter table public.todos enable row level security;\ncreate policy \"todos any user\" on public.todos for all to authenticated using (auth.uid() is not null);\n\ncreate table public.payments (id bigint primary key, user_id uuid, amount_cents int);\n\ncreate table public.posts (id bigint primary key, body text);\nalter table public.posts enable row level security;\ncreate policy \"public posts\" on public.posts for select using (true);\n"
  23. 23 },
  24. 24 "checks": [
  25. 25 {
  26. 26 "type": "command_exits_zero",
  27. 27 "command": "node scripts/rls-audit.mjs supabase/migrations --json out/rls-audit.json --md out/rls-audit.md"
  28. 28 },
  29. 29 {
  30. 30 "type": "output_matches_schema",
  31. 31 "path": "out/rls-audit.json",
  32. 32 "schema": {
  33. 33 "type": "object",
  34. 34 "required": ["summary", "tables", "findings"],
  35. 35 "properties": {
  36. 36 "summary": {
  37. 37 "type": "object",
  38. 38 "properties": {
  39. 39 "tables": { "const": 3 },
  40. 40 "rls_enabled": { "const": 2 },
  41. 41 "high": { "const": 2 },
  42. 42 "medium": { "const": 1 }
  43. 43 }
  44. 44 }
  45. 45 }
  46. 46 }
  47. 47 },
  48. 48 { "type": "file_contains", "path": "out/rls-audit.md", "text": "| public.payments | **OFF** | 0 |" },
  49. 49 { "type": "file_contains", "path": "out/rls-audit.md", "text": "policy-any-user-write" }
  50. 50 ]
  51. 51 },
  52. 52 {
  53. 53 "name": "replays later migrations: a fixed table and a dropped policy come out clean",
  54. 54 "inputs": {
  55. 55 "supabase/migrations/0001_init.sql": "create table public.notes (id bigint primary key, user_id uuid not null, body text);\nalter table public.notes enable row level security;\ncreate policy \"open notes\" on public.notes for all using (true) with check (true);\n",
  56. 56 "supabase/migrations/0002_fix.sql": "-- tighten notes\ndrop policy if exists \"open notes\" on public.notes;\ncreate policy \"own notes\" on public.notes for all to authenticated\n using ((select auth.uid()) = user_id)\n with check ((select auth.uid()) = user_id);\n"
  57. 57 },
  58. 58 "checks": [
  59. 59 {
  60. 60 "type": "command_exits_zero",
  61. 61 "command": "node scripts/rls-audit.mjs supabase/migrations --json out/rls-audit.json"
  62. 62 },
  63. 63 {
  64. 64 "type": "output_matches_schema",
  65. 65 "path": "out/rls-audit.json",
  66. 66 "schema": {
  67. 67 "type": "object",
  68. 68 "properties": {
  69. 69 "summary": {
  70. 70 "type": "object",
  71. 71 "properties": {
  72. 72 "tables": { "const": 1 },
  73. 73 "high": { "const": 0 },
  74. 74 "medium": { "const": 0 },
  75. 75 "low": { "const": 0 }
  76. 76 }
  77. 77 },
  78. 78 "findings": { "type": "array", "maxItems": 0 }
  79. 79 }
  80. 80 }
  81. 81 }
  82. 82 ]
  83. 83 },
  84. 84 {
  85. 85 "name": "catches user_metadata trust, definer views, and unpinned SECURITY DEFINER functions",
  86. 86 "inputs": {
  87. 87 "supabase/migrations/0001.sql": "create table public.orders (id bigint primary key, user_id uuid);\nalter table public.orders enable row level security;\ncreate policy \"admins\" on public.orders for select to authenticated using ((auth.jwt() -> 'user_metadata' ->> 'role') = 'admin');\ncreate view public.order_totals as select user_id, count(*) from public.orders group by user_id;\ncreate function public.whoami() returns uuid language sql security definer as $$ select auth.uid(); $$;\n"
  88. 88 },
  89. 89 "checks": [
  90. 90 {
  91. 91 "type": "command_exits_zero",
  92. 92 "command": "node scripts/rls-audit.mjs supabase/migrations --json out/rls-audit.json --md out/rls-audit.md"
  93. 93 },
  94. 94 { "type": "file_contains", "path": "out/rls-audit.md", "text": "policy-user-metadata" },
  95. 95 { "type": "file_contains", "path": "out/rls-audit.md", "text": "view-security-definer" },
  96. 96 { "type": "file_contains", "path": "out/rls-audit.md", "text": "security-definer-function" }
  97. 97 ]
  98. 98 }
  99. 99 ]
  100. 100 }