From 8f1d4fd58cc898e1fbf53247e832d4c7990787c9 Mon Sep 17 00:00:00 2001 From: djedi-knight Date: Thu, 15 Oct 2026 14:57:13 -0400 Subject: [PATCH] fix(0003,0006,0015): lint user-authored RLS policies in storage and realtime Users write their own RLS policies on storage.objects and realtime.messages, but the policy-content lints excluded those schemas entirely, so slow or insecure policies there went unreported (#35). Following the precedent of #88 (0003 on realtime), drop the schemas from the exclusion lists of the lints that inspect policy content: - 0003 auth_rls_initplan: add storage (realtime already enabled) - 0006 multiple_permissive_policies: add storage - 0015 rls_references_user_metadata: add storage and realtime Lints about table configuration (0007, 0008, 0013) stay excluded, since users can't change RLS settings on Supabase-owned tables, and auth stays excluded everywhere. 0006 is not re-enabled on realtime: it only inspects relkind 'r', and realtime.messages is partitioned. Tests cover the new storage/realtime findings and that auth remains excluded. splinter.sql regenerated with bin/compile.py. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017TAbfjJfpipDmD7NJuQmPx --- lints/0003_auth_rls_initplan.sql | 5 ++- lints/0006_multiple_permissive_policies.sql | 2 +- lints/0015_rls_references_user_metadata.sql | 2 +- splinter.sql | 9 ++-- test/expected/0003_auth_rls_initplan.out | 30 +++++++++++++ .../0006_multiple_permissive_policies.out | 43 +++++++++++++++++++ .../0015_rls_references_user_metadata.out | 37 ++++++++++++++++ test/sql/0003_auth_rls_initplan.sql | 31 +++++++++++++ .../sql/0006_multiple_permissive_policies.sql | 43 +++++++++++++++++++ .../sql/0015_rls_references_user_metadata.sql | 35 +++++++++++++++ 10 files changed, 229 insertions(+), 8 deletions(-) diff --git a/lints/0003_auth_rls_initplan.sql b/lints/0003_auth_rls_initplan.sql index ee53e6be..f056fbc4 100644 --- a/lints/0003_auth_rls_initplan.sql +++ b/lints/0003_auth_rls_initplan.sql @@ -52,9 +52,10 @@ from policies where is_rls_active - -- NOTE: does not include realtime in support of monitoring policies on realtime.messages + -- NOTE: does not include realtime or storage in support of monitoring user-authored + -- policies on realtime.messages and storage.objects and schema_name not in ( - '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' + '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'repack', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' ) and ( -- Example: auth.uid() diff --git a/lints/0006_multiple_permissive_policies.sql b/lints/0006_multiple_permissive_policies.sql index a89dd617..311b7ec2 100644 --- a/lints/0006_multiple_permissive_policies.sql +++ b/lints/0006_multiple_permissive_policies.sql @@ -59,7 +59,7 @@ where c.relkind = 'r' -- regular tables and p.polpermissive -- policy is permissive and n.nspname not in ( - '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' + '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'realtime', 'repack', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' ) and r.rolname not like 'pg_%' and r.rolname not like 'supabase%admin' diff --git a/lints/0015_rls_references_user_metadata.sql b/lints/0015_rls_references_user_metadata.sql index f5dc8fa2..a86a2c82 100644 --- a/lints/0015_rls_references_user_metadata.sql +++ b/lints/0015_rls_references_user_metadata.sql @@ -42,7 +42,7 @@ from policies where schema_name not in ( - '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' + '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'repack', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' ) and ( -- Example: auth.jwt() -> 'user_metadata' diff --git a/splinter.sql b/splinter.sql index 46bb331e..fe3dcf82 100644 --- a/splinter.sql +++ b/splinter.sql @@ -241,9 +241,10 @@ from policies where is_rls_active - -- NOTE: does not include realtime in support of monitoring policies on realtime.messages + -- NOTE: does not include realtime or storage in support of monitoring user-authored + -- policies on realtime.messages and storage.objects and schema_name not in ( - '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' + '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'repack', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' ) and ( -- Example: auth.uid() @@ -440,7 +441,7 @@ where c.relkind = 'r' -- regular tables and p.polpermissive -- policy is permissive and n.nspname not in ( - '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' + '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'realtime', 'repack', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' ) and r.rolname not like 'pg_%' and r.rolname not like 'supabase%admin' @@ -821,7 +822,7 @@ from policies where schema_name not in ( - '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' + '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'repack', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' ) and ( -- Example: auth.jwt() -> 'user_metadata' diff --git a/test/expected/0003_auth_rls_initplan.out b/test/expected/0003_auth_rls_initplan.out index b5de9a4a..70ebb1d5 100644 --- a/test/expected/0003_auth_rls_initplan.out +++ b/test/expected/0003_auth_rls_initplan.out @@ -90,4 +90,34 @@ begin; auth_rls_initplan | Auth RLS Initialization Plan | WARN | EXTERNAL | {PERFORMANCE} | Detects if calls to \`current_setting()\` and \`auth.()\` in RLS policies are being unnecessarily re-evaluated for each row | Table \`realtime.messages\` has a row level security policy \`realtime_check\` that re-evaluates current_setting() or auth.() for each row. This produces suboptimal query performance at scale. Resolve the issue by replacing \`auth.()\` with \`(select auth.())\`. See [docs](https://supabase.com/docs/guides/database/postgres/row-level-security#call-functions-with-select) for more info. | https://supabase.com/docs/guides/database/database-linter?lint=0003_auth_rls_initplan | {"name": "messages", "type": "table", "schema": "realtime"} | auth_rls_init_plan_realtime_messages_realtime_check (1 row) + rollback to savepoint a; + -- Confirm that storage.objects will be scanned (#35) + create schema storage; + create table storage.objects( id int primary key, owner uuid); + create policy storage_owner_check on storage.objects + for select + to authenticated + using (owner = auth.uid()); + alter table storage.objects enable row level security; + -- 1 entry for storage.objects + select * from lint."0003_auth_rls_initplan"; + name | title | level | facing | categories | description | detail | remediation | metadata | cache_key +-------------------+------------------------------+-------+----------+---------------+---------------------------------------------------------------------------------------------------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------+-----------------------------------------------------------+-------------------------------------------------------- + auth_rls_initplan | Auth RLS Initialization Plan | WARN | EXTERNAL | {PERFORMANCE} | Detects if calls to \`current_setting()\` and \`auth.()\` in RLS policies are being unnecessarily re-evaluated for each row | Table \`storage.objects\` has a row level security policy \`storage_owner_check\` that re-evaluates current_setting() or auth.() for each row. This produces suboptimal query performance at scale. Resolve the issue by replacing \`auth.()\` with \`(select auth.())\`. See [docs](https://supabase.com/docs/guides/database/postgres/row-level-security#call-functions-with-select) for more info. | https://supabase.com/docs/guides/database/database-linter?lint=0003_auth_rls_initplan | {"name": "objects", "type": "table", "schema": "storage"} | auth_rls_init_plan_storage_objects_storage_owner_check +(1 row) + + rollback to savepoint a; + -- auth remains excluded: users do not author policies there + create table auth.sessions( id int primary key, user_id uuid); + create policy auth_sessions_check on auth.sessions + for select + to authenticated + using (user_id = auth.uid()); + alter table auth.sessions enable row level security; + -- No issues + select * from lint."0003_auth_rls_initplan"; + name | title | level | facing | categories | description | detail | remediation | metadata | cache_key +------+-------+-------+--------+------------+-------------+--------+-------------+----------+----------- +(0 rows) + rollback; diff --git a/test/expected/0006_multiple_permissive_policies.out b/test/expected/0006_multiple_permissive_policies.out index b6b2d067..80c8682f 100644 --- a/test/expected/0006_multiple_permissive_policies.out +++ b/test/expected/0006_multiple_permissive_policies.out @@ -39,6 +39,49 @@ begin; select * from lint."0006_multiple_permissive_policies"; name | title | level | facing | categories | description | detail | remediation | metadata | cache_key ------+-------+-------+--------+------------+-------------+--------+-------------+----------+----------- +(0 rows) + + savepoint a; + -- Confirm that storage.objects will be scanned (#35) + create schema storage; + create table storage.objects( + id int primary key, + bucket_id text + ); + alter table storage.objects enable row level security; + create policy avatars_read on storage.objects + for select + to authenticated + using (bucket_id = 'avatars'); + create policy documents_read on storage.objects + for select + to authenticated + using (bucket_id = 'documents'); + -- 1 entry for storage.objects, authenticated, SELECT + select * from lint."0006_multiple_permissive_policies"; + name | title | level | facing | categories | description | detail | remediation | metadata | cache_key +------------------------------+------------------------------+-------+----------+---------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------+-----------------------------------------------------------+------------------------------------------------------------------- + multiple_permissive_policies | Multiple Permissive Policies | WARN | EXTERNAL | {PERFORMANCE} | Detects if multiple permissive row level security policies are present on a table for the same \`role\` and \`action\` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query. | Table \`storage.objects\` has multiple permissive policies for role \`authenticated\` for action \`SELECT\`. Policies include \`{avatars_read,documents_read}\` | https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies | {"name": "objects", "type": "table", "schema": "storage"} | multiple_permissive_policies_storage_objects_authenticated_SELECT +(1 row) + + rollback to savepoint a; + -- auth remains excluded: users do not author policies there + create table auth.sessions( + id int primary key + ); + alter table auth.sessions enable row level security; + create policy sessions_read_1 on auth.sessions + for select + to authenticated + using (true); + create policy sessions_read_2 on auth.sessions + for select + to authenticated + using (true); + -- No issues + select * from lint."0006_multiple_permissive_policies"; + name | title | level | facing | categories | description | detail | remediation | metadata | cache_key +------+-------+-------+--------+------------+-------------+--------+-------------+----------+----------- (0 rows) rollback; diff --git a/test/expected/0015_rls_references_user_metadata.out b/test/expected/0015_rls_references_user_metadata.out index f42a3619..da3041f8 100644 --- a/test/expected/0015_rls_references_user_metadata.out +++ b/test/expected/0015_rls_references_user_metadata.out @@ -34,4 +34,41 @@ begin; rls_references_user_metadata | RLS references user metadata | ERROR | EXTERNAL | {SECURITY} | Detects when Supabase Auth user_metadata is referenced insecurely in a row level security (RLS) policy. | Table \`public.foo\` has a row level security policy \`bad_policy_2\` that references Supabase Auth \`user_metadata\`. \`user_metadata\` is editable by end users and should never be used in a security context. | https://supabase.com/docs/guides/database/database-linter?lint=0015_rls_references_user_metadata | {"name": "foo", "type": "table", "schema": "public"} | rls_references_user_metadata_public_foo_bad_policy_2 (2 rows) + savepoint a; + drop table public.foo; + -- Confirm that storage.objects and realtime.messages will be scanned (#35) + create schema storage; + create table storage.objects( id int primary key); + create policy storage_user_metadata on storage.objects + for select + to authenticated + using ( (( select auth.jwt() ) -> 'user_metadata' ->> 'is_admin' )::bool ); + create schema realtime; + create table realtime.messages( id int primary key); + create policy realtime_user_metadata on realtime.messages + for select + to authenticated + using ( (( select auth.jwt() ) -> 'user_metadata' ->> 'is_admin' )::bool ); + -- 2 errors, one each for storage.objects and realtime.messages + select * from lint."0015_rls_references_user_metadata"; + name | title | level | facing | categories | description | detail | remediation | metadata | cache_key +------------------------------+------------------------------+-------+----------+------------+---------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------+-------------------------------------------------------------+----------------------------------------------------------------------- + rls_references_user_metadata | RLS references user metadata | ERROR | EXTERNAL | {SECURITY} | Detects when Supabase Auth user_metadata is referenced insecurely in a row level security (RLS) policy. | Table \`storage.objects\` has a row level security policy \`storage_user_metadata\` that references Supabase Auth \`user_metadata\`. \`user_metadata\` is editable by end users and should never be used in a security context. | https://supabase.com/docs/guides/database/database-linter?lint=0015_rls_references_user_metadata | {"name": "objects", "type": "table", "schema": "storage"} | rls_references_user_metadata_storage_objects_storage_user_metadata + rls_references_user_metadata | RLS references user metadata | ERROR | EXTERNAL | {SECURITY} | Detects when Supabase Auth user_metadata is referenced insecurely in a row level security (RLS) policy. | Table \`realtime.messages\` has a row level security policy \`realtime_user_metadata\` that references Supabase Auth \`user_metadata\`. \`user_metadata\` is editable by end users and should never be used in a security context. | https://supabase.com/docs/guides/database/database-linter?lint=0015_rls_references_user_metadata | {"name": "messages", "type": "table", "schema": "realtime"} | rls_references_user_metadata_realtime_messages_realtime_user_metadata +(2 rows) + + rollback to savepoint a; + drop table public.foo; + -- auth remains excluded: users do not author policies there + create table auth.sessions( id int primary key); + create policy auth_user_metadata on auth.sessions + for select + to authenticated + using ( (( select auth.jwt() ) -> 'user_metadata' ->> 'is_admin' )::bool ); + -- No issues + select * from lint."0015_rls_references_user_metadata"; + name | title | level | facing | categories | description | detail | remediation | metadata | cache_key +------+-------+-------+--------+------------+-------------+--------+-------------+----------+----------- +(0 rows) + rollback; diff --git a/test/sql/0003_auth_rls_initplan.sql b/test/sql/0003_auth_rls_initplan.sql index e00969f0..1050877f 100644 --- a/test/sql/0003_auth_rls_initplan.sql +++ b/test/sql/0003_auth_rls_initplan.sql @@ -91,4 +91,35 @@ begin; -- 1 entry for realtime.messages select * from lint."0003_auth_rls_initplan"; + rollback to savepoint a; + + -- Confirm that storage.objects will be scanned (#35) + create schema storage; + create table storage.objects( id int primary key, owner uuid); + + create policy storage_owner_check on storage.objects + for select + to authenticated + using (owner = auth.uid()); + + alter table storage.objects enable row level security; + + -- 1 entry for storage.objects + select * from lint."0003_auth_rls_initplan"; + + rollback to savepoint a; + + -- auth remains excluded: users do not author policies there + create table auth.sessions( id int primary key, user_id uuid); + + create policy auth_sessions_check on auth.sessions + for select + to authenticated + using (user_id = auth.uid()); + + alter table auth.sessions enable row level security; + + -- No issues + select * from lint."0003_auth_rls_initplan"; + rollback; diff --git a/test/sql/0006_multiple_permissive_policies.sql b/test/sql/0006_multiple_permissive_policies.sql index 88c24d8e..cca2e2a9 100644 --- a/test/sql/0006_multiple_permissive_policies.sql +++ b/test/sql/0006_multiple_permissive_policies.sql @@ -37,7 +37,50 @@ begin; -- No issues, the policies are restrictive, which is fine select * from lint."0006_multiple_permissive_policies"; + savepoint a; + -- Confirm that storage.objects will be scanned (#35) + create schema storage; + create table storage.objects( + id int primary key, + bucket_id text + ); + + alter table storage.objects enable row level security; + + create policy avatars_read on storage.objects + for select + to authenticated + using (bucket_id = 'avatars'); + + create policy documents_read on storage.objects + for select + to authenticated + using (bucket_id = 'documents'); + -- 1 entry for storage.objects, authenticated, SELECT + select * from lint."0006_multiple_permissive_policies"; + + rollback to savepoint a; + + -- auth remains excluded: users do not author policies there + create table auth.sessions( + id int primary key + ); + + alter table auth.sessions enable row level security; + + create policy sessions_read_1 on auth.sessions + for select + to authenticated + using (true); + + create policy sessions_read_2 on auth.sessions + for select + to authenticated + using (true); + + -- No issues + select * from lint."0006_multiple_permissive_policies"; rollback; diff --git a/test/sql/0015_rls_references_user_metadata.sql b/test/sql/0015_rls_references_user_metadata.sql index 3209b0e4..30291259 100644 --- a/test/sql/0015_rls_references_user_metadata.sql +++ b/test/sql/0015_rls_references_user_metadata.sql @@ -31,6 +31,41 @@ begin; -- 2 errors, both 'bad_*' policies select * from lint."0015_rls_references_user_metadata"; + savepoint a; + drop table public.foo; + -- Confirm that storage.objects and realtime.messages will be scanned (#35) + create schema storage; + create table storage.objects( id int primary key); + + create policy storage_user_metadata on storage.objects + for select + to authenticated + using ( (( select auth.jwt() ) -> 'user_metadata' ->> 'is_admin' )::bool ); + + create schema realtime; + create table realtime.messages( id int primary key); + + create policy realtime_user_metadata on realtime.messages + for select + to authenticated + using ( (( select auth.jwt() ) -> 'user_metadata' ->> 'is_admin' )::bool ); + + -- 2 errors, one each for storage.objects and realtime.messages + select * from lint."0015_rls_references_user_metadata"; + + rollback to savepoint a; + drop table public.foo; + + -- auth remains excluded: users do not author policies there + create table auth.sessions( id int primary key); + + create policy auth_user_metadata on auth.sessions + for select + to authenticated + using ( (( select auth.jwt() ) -> 'user_metadata' ->> 'is_admin' )::bool ); + + -- No issues + select * from lint."0015_rls_references_user_metadata"; rollback;