Compare commits

...
Author SHA1 Message Date
Wintermute b73ddc8c15 fix(doctor): check ALL public tables for RLS, not just gbrain's own
The RLS check was hardcoded to only verify 10 gbrain-managed tables:
pages, content_chunks, links, tags, raw_data, page_versions,
timeline_entries, ingest_log, config, files.

Any other table in the public schema (created by the application,
extensions, or manually) was invisible to the check. This allowed
12 tables to exist without RLS for months — publicly readable by
anyone with the Supabase anon key.

Changes:
- Query ALL tables in public schema, not a hardcoded list
- Upgrade severity from 'warn' to 'fail' — missing RLS is a security
  issue, not a suggestion
- Include table count in success message for visibility
- Include remediation SQL in failure message

Supabase exposes the public schema via PostgREST. Any table without
RLS is readable/writable by the anon key by default.
2026-04-22 19:11:21 +00:00
+11 -5
View File
@@ -276,22 +276,28 @@ export async function runDoctor(engine: BrainEngine | null, args: string[], dbSo
// best-effort; never fail doctor on this check
}
// 5. RLS
// 5. RLS — check ALL public tables, not just gbrain's own.
// Any table without RLS in the public schema is a security risk:
// Supabase exposes the public schema via PostgREST, so tables without
// RLS are readable/writable by anyone with the anon key.
progress.heartbeat('rls');
try {
const sql = db.getConnection();
const tables = await sql`
SELECT tablename, rowsecurity FROM pg_tables
WHERE schemaname = 'public'
AND tablename IN ('pages','content_chunks','links','tags','raw_data',
'page_versions','timeline_entries','ingest_log','config','files')
`;
const noRls = tables.filter((t: any) => !t.rowsecurity);
if (noRls.length === 0) {
checks.push({ name: 'rls', status: 'ok', message: 'RLS enabled on all tables' });
checks.push({ name: 'rls', status: 'ok', message: `RLS enabled on all ${tables.length} public tables` });
} else {
const names = noRls.map((t: any) => t.tablename).join(', ');
checks.push({ name: 'rls', status: 'warn', message: `RLS not enabled on: ${names}` });
checks.push({
name: 'rls',
status: 'fail',
message: `${noRls.length} table(s) WITHOUT Row Level Security: ${names}. `
+ `Fix: ALTER TABLE public.<name> ENABLE ROW LEVEL SECURITY;`,
});
}
} catch {
checks.push({ name: 'rls', status: 'warn', message: 'Could not check RLS status' });