skills/security/rls-checker/SKILL.md
Use this skill when the user says 'check RLS', 'audit RLS', 'RLS policies', 'row level security', 'Supabase security audit', or needs to verify table-level access control. Audits Supabase Row Level Security policies across all tables. Do NOT use for non-Supabase projects or writing RLS policies from scratch.
npx skillsauth add cwinvestments/memstack memstack-security-rls-checkerInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Audit Supabase Row Level Security policies across all tables in a project.
When this skill activates, output:
🔒 RLS Checker — Auditing Row Level Security...
Then execute the protocol below.
| Context | Status | |---------|--------| | User asks to check/audit RLS | ACTIVE — full audit | | User mentions Supabase security | ACTIVE — full audit | | User asks about table permissions | ACTIVE — full audit | | User is writing RLS policies | DORMANT — they know what they're doing | | Non-Supabase project | DORMANT — not applicable |
Find all Supabase tables referenced in the project. Search in priority order:
Migration files — most authoritative source:
find . -path "*/migrations/*.sql" -o -path "*/supabase/migrations/*.sql" | head -50
Look for CREATE TABLE statements.
Generated types — comprehensive if available:
types/database.ts, types/supabase.ts, src/types/database.types.ts, database.types.ts
Parse the Tables interface for all table names.
Client usage — catches tables missed by above:
grep -r "\.from(['\"]" --include="*.ts" --include="*.tsx" --include="*.js"
Extract table names from .from('table_name') calls.
Storage buckets — separate RLS surface:
grep -r "storage\.from\|createBucket\|storage-api" --include="*.ts" --include="*.tsx" --include="*.sql"
Compile a deduplicated list of all tables and storage buckets.
For each table, find its RLS configuration:
Search migration SQL for RLS statements:
ALTER TABLE <name> ENABLE ROW LEVEL SECURITY — RLS is onCREATE POLICY statements — extract policy name, operation (SELECT/INSERT/UPDATE/DELETE/ALL), and USING/WITH CHECK expressionsALTER TABLE <name> FORCE ROW LEVEL SECURITY — RLS enforced even for table ownersCheck for intentionally unprotected tables:
GRANT SELECT ON <table> TO anon without RLS are intentionally public-- public table, -- no RLS needed, or -- rls:skip in migration SQL-- rls:skip should be classified as ✅ OK (Intentional) in the report, not flagged as missing RLS. This lets teams explicitly document tables that rely on application-level authorization (e.g., service-role-first architectures).-- rls:skip marker exists and no RLS is enabled, flag normally.Check Supabase dashboard-configured policies:
supabase inspect db policies or the Dashboard UI."supabase/config.toml, .supabase/) that might indicate whether the project uses Dashboard-managed policies.supabase db dump --schema public --data-only=false | grep -A5 "CREATE POLICY"
Check Supabase dashboard seed/init files for policy definitions that may not be in migrations.
For each table with RLS enabled, evaluate policy quality:
Check 1 — Operation Coverage: Flag tables missing policies for any CRUD operation:
Check 2 — User Isolation: Verify policies filter by authenticated user:
auth.uid() in USING clause — standard user isolation (OK)auth.uid() in WITH CHECK clause — write isolation (OK)auth.uid() reference — overly permissive (WARNING)auth.uid() — security risk (CRITICAL)current_setting('app.*') instead of auth.uid() — anti-pattern (WARNING). This relies on the application explicitly setting a PostgreSQL session variable before every query. If the variable is unset, the policy may fail open or closed unpredictably. Prefer auth.uid() which Supabase populates automatically from the JWT. Flag with:
grep -rn "current_setting" --include="*.sql"
Check 3 — Multi-Tenant Isolation:
For tables with organization_id or team_id columns:
organization_id = <value> without membership check is insufficient (WARNING)CREATE POLICY "org_isolation" ON documents
USING (organization_id IN (
SELECT org_id FROM organization_members
WHERE user_id = auth.uid()
));
Check 4 — Service Role Bypass: Search codebase for service role usage that bypasses RLS:
grep -r "service_role\|serviceRole\|supabaseAdmin\|SUPABASE_SERVICE_ROLE" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.env*"
.env committed to git — critical vulnerability (CRITICAL)Check 5 — Storage Bucket Policies: For each storage bucket found:
auth.uid()::text = (storage.foldername(name))[1])If the project uses service role for most/all database access (Check 4 found widespread supabaseAdmin / SUPABASE_SERVICE_ROLE usage), compute a defense-in-depth score:
Score calculation:
(tables with RLS / total sensitive tables) × 100Classification: | Score | Rating | Meaning | |-------|--------|---------| | 80–100% | 🟢 Strong | RLS provides meaningful backup even though service role bypasses it | | 50–79% | 🟡 Partial | Some defense-in-depth but gaps remain | | 20–49% | 🟠 Weak | Most sensitive tables unprotected at DB layer | | 0–19% | 🔴 None | Entire security model depends on application code — single bug = full breach |
Include in report:
## Defense-in-Depth Score
Architecture: Service-role-first (all API routes use service role key)
Sensitive tables: <count>
Sensitive tables with RLS: <count>
Score: <percentage> — <rating>
Note: Service role bypasses RLS by design. This score measures how well
the database would protect data if an application-level auth bug occurred.
Recommendation for low scores: Even in service-role architectures, enabling RLS on sensitive tables provides a safety net. If a developer accidentally uses the anon key, creates a new route without auth, or a future refactor introduces a bug, RLS prevents cross-tenant data access at the database layer.
Output a structured report with this format:
🔒 RLS Audit Report
Project: <project-name>
Tables found: <count>
Storage buckets: <count>
## Table Audit
| Table | RLS | Policies | Coverage | Risk | Issue |
|-------|-----|----------|----------|------|-------|
| users | ON | 4 | Full | ✅ OK | — |
| documents | ON | 2 | Partial | ⚠️ WARN | Missing DELETE policy |
| payments | OFF | 0 | None | 🔴 CRIT | No RLS enabled |
| public_posts | OFF | 0 | N/A | ✅ OK | Intentionally public (-- rls:skip) |
## Storage Buckets
| Bucket | Policies | Risk | Issue |
|--------|----------|------|-------|
| avatars | 2 | ✅ OK | — |
| uploads | 0 | ⚠️ WARN | No upload restriction |
## Critical Issues
1. **payments** — No RLS enabled. Any authenticated user can read/write all rows.
→ Fix: `ALTER TABLE payments ENABLE ROW LEVEL SECURITY;` then add user-scoped policies.
2. **service_role in client** — Found in `src/lib/supabase.ts:14`.
→ Fix: Remove service role key from client code. Use server-side API route instead.
## Warnings
1. **documents** — Missing DELETE policy. Users may not be able to delete their own documents, or deletion may be unrestricted.
→ Fix: Add `CREATE POLICY "delete_own" ON documents FOR DELETE USING (user_id = auth.uid());`
2. **uploads bucket** — No storage policies defined.
→ Fix: Add bucket policies restricting uploads to user-specific paths.
## Summary
- 🔴 Critical: <count>
- ⚠️ Warning: <count>
- ✅ OK: <count>
- Total tables: <count>
For each CRITICAL and WARNING issue, provide:
Offer to generate a migration file with all fixes: supabase/migrations/<timestamp>_rls_fixes.sql
| Level | Meaning | Action | |-------|---------|--------| | 🔴 CRITICAL | Data exposed or writable by unauthorized users | Fix immediately | | ⚠️ WARNING | Incomplete coverage or weak isolation | Fix before production | | ℹ️ INFO | Acceptable pattern that should be verified | Review and confirm intentional | | ✅ OK | Properly secured | No action needed |
User-owned rows:
CREATE POLICY "users_own_data" ON table_name
FOR ALL USING (user_id = auth.uid())
WITH CHECK (user_id = auth.uid());
Org-scoped with membership check:
CREATE POLICY "org_members_access" ON table_name
FOR ALL USING (
organization_id IN (
SELECT org_id FROM organization_members
WHERE user_id = auth.uid()
)
);
Public read, authenticated write:
CREATE POLICY "public_read" ON table_name FOR SELECT USING (true);
CREATE POLICY "auth_insert" ON table_name FOR INSERT WITH CHECK (auth.role() = 'authenticated');
Storage bucket user isolation:
CREATE POLICY "user_uploads" ON storage.objects
FOR INSERT WITH CHECK (
bucket_id = 'uploads' AND
auth.uid()::text = (storage.foldername(name))[1]
);
current_setting() anti-pattern detection (vs auth.uid()), -- rls:skip marker for intentionally unprotected tables, Supabase Dashboard policy detection guidance (supabase inspect db policies), defense-in-depth score for service-role-first architectures. (Origin: AdminStack audit, Mar 2026)tools
Use when the user says 'save diary', 'log session', 'wrapping up', or at end of a productive session.
tools
Use when the user says 'submit to marketplace', 'publish my skill', 'share this skill', 'list on marketplace', 'submit plugin', 'publish to community', or needs to submit a skill or plugin to a community marketplace via PR. Do NOT use for building skills or writing plugin code.
development
Use when the user says 'write browser tests', 'test this page', 'playwright test', 'e2e test', 'end to end test', 'browser test', 'test the UI', or needs Playwright-based browser testing for a web application. Do NOT use for unit tests, API tests, or non-browser testing.
development
Use when the user says 'teach me', 'explain as you go', 'mentor mode', 'walk me through', 'help me learn', 'explain why', 'learning mode', or wants real-time plain language narration of decisions and tradeoffs while building. Do NOT use for code review or debugging.