Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion bin/installcheck
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,12 @@ else
fi

# Execute the test fixtures
psql -v ON_ERROR_STOP= -f test/fixtures.sql -f lints/0001*.sql -f lints/0002*.sql -f lints/0003*.sql -f lints/0004*.sql -f lints/0005*.sql -f lints/0006*.sql -f lints/0007*.sql -f lints/0008*.sql -f lints/0009*.sql -f lints/0010*.sql -f lints/0011*.sql -f lints/0013*.sql -d contrib_regression
psql -v ON_ERROR_STOP= -f test/fixtures.sql -f lints/0001*.sql -f lints/0002*.sql -f lints/0003*.sql -f lints/0004*.sql -f lints/0005*.sql -f lints/0006*.sql -f lints/0007*.sql -f lints/0008*.sql -f lints/0009*.sql -f lints/0010*.sql -f lints/0011*.sql -f lints/0012*.sql -f lints/0013*.sql -d contrib_regression

# Run tests
${REGRESS} --use-existing --dbname=contrib_regression --inputdir=${TESTDIR} ${TESTS}

if [ -s /home/splinter/regression.diffs ]
then
cat /home/splinter/regression.diffs
fi
31 changes: 31 additions & 0 deletions docs/0012_auth_allow_anonymous_sign_ins.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
Level: INFO

### Rationale

Anonymous users use the same `authenticated` Postgres role as permanent users when accessing the database. If you have enabled anonymous sign-in for your project, existing RLS policies may allow unintended access to an anonymous user's JWT.

### Difference between an anonymous user and a permanent user

An anonymous user is a user created through Supabase Auth. It is just like a permanent user, except the user can't access their account if they sign out, clear browsing data or use another device. An anonymous user can be differentiated from a permanent user by checking if the `is_anonymous` claim is true. These claims are returned by the `auth.jwt()` function.

### How to Resolve

Determine if existing row level security (RLS) policies are meant to allow access to anonymous users. Affected policies include those that are associated to the `authenticated` or `public` roles, and members of those roles that inherit privileges.

For example, consider the policy:

```sql
create policy "allow_access_to_authenticated" on documents
as restrictive
to authenticated
using (true);
```

In this policy, any JWT that contains the authenticated role will be allowed to access the documents table. If we want to restrict access to permanent users only, we can modify the policy to:

```sql
create policy "allow_access_to_permanent_users" on documents
as restrictive
to authenticated
using ( (select (auth.jwt()->>'is_anonymous')::boolean) is false );
```
56 changes: 56 additions & 0 deletions lints/0012_auth_allow_anonymous_sign_ins.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
create view lint."0012_auth_allow_anonymous_sign_ins" as

with recursive role_members as (
select
roleid,
member
from pg_catalog.pg_auth_members
where roleid = (select oid from pg_roles where rolname = 'authenticated')
union
select
am.roleid,
am.member
from pg_catalog.pg_auth_members as am
inner join role_members as rm on am.roleid = rm.member
),

member_names as (
select r.rolname from pg_roles as r
inner join role_members as m on r.oid = m.member
)

select
'auth_allow_anonymous_sign_ins' as name,
'INFO' as level,
'EXTERNAL' as facing,
'Detects row level security (RLS) policies that allow access to anonymous users.' as description,
'https://supabase.github.io/splinter/0012_auth_allow_anonymous_sign_ins' as remediation,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you change this to a main docs site link please? Following the pattern here: https://supabase.com/docs/guides/database/database-advisors

format(
'Table \`%s.%s\` has policies enforced on roles that allow access to anonymous users. Policies include \`%s\`',
n.nspname,
c.relname,
array_agg(p.policyname order by p.policyname)
) as detail,
jsonb_build_object(
'schema', n.nspname,
'name', c.relname,
'type', 'table'
) as metadata,
format(
'auth_allow_anonymous_sign_ins_%s_%s',
n.nspname,
c.relname
) as cache_key
from pg_catalog.pg_policies as p
inner join pg_catalog.pg_class as c on p.tablename = c.relname
inner join pg_catalog.pg_namespace as n on c.relnamespace = n.oid
where
(
p.roles = array['public'::name] -- public roles
or p.roles = array['authenticated'::name] -- authenticated roles
or exists (
select rolname from member_names where rolname = any(roles)
) -- roles that are members of authenticated
)
and replace(p.qual, ' ', '') not like '%auth.jwt()%->>%is_anonymous%'
group by n.nspname, c.relname
1 change: 1 addition & 0 deletions splinter.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@
"0009_duplicate_index": "(\nselect\n 'duplicate_index' as name,\n 'WARN' as level,\n 'EXTERNAL' as facing,\n 'Detects cases where two ore more identical indexes exist.' as description,\n format(\n 'Table \\`%s.%s\\` has identical indexes %s. Drop all except one of them',\n n.nspname,\n c.relname,\n array_agg(pi.indexname order by pi.indexname)\n ) as detail,\n 'https://supabase.github.io/splinter/0009_duplicate_index' as remediation,\n jsonb_build_object(\n 'schema', n.nspname,\n 'name', c.relname,\n 'type', case\n when c.relkind = 'r' then 'table'\n when c.relkind = 'm' then 'materialized view'\n else 'ERROR'\n end,\n 'indexes', array_agg(pi.indexname order by pi.indexname)\n ) as metadata,\n format(\n 'duplicate_index_%s_%s_%s',\n n.nspname,\n c.relname,\n array_agg(pi.indexname order by pi.indexname)\n ) as cache_key\nfrom\n pg_catalog.pg_indexes pi\n join pg_catalog.pg_namespace n\n on n.nspname = pi.schemaname\n join pg_catalog.pg_class c\n on pi.tablename = c.relname\n and n.oid = c.relnamespace\n left join pg_catalog.pg_policy p\n on p.polrelid = c.oid\nwhere\n c.relkind in ('r', 'm') -- tables and materialized views\n and n.nspname not in (\n 'pg_catalog', 'information_schema', 'auth', 'storage', 'vault', 'pgsodium'\n )\ngroup by\n n.nspname,\n c.relkind,\n c.relname,\n replace(pi.indexdef, pi.indexname, '')\nhaving\n count(*) > 1)",
"0010_security_definer_view": "(\nselect\n 'security_definer_view' as name,\n 'WARN' as level,\n 'EXTERNAL' as facing,\n 'Detects views that are SECURITY DEFINER meaning that they ignore row level security (RLS) policies.' as description,\n format(\n 'View \\`%s.%s\\` is SECURITY DEFINER',\n n.nspname,\n c.relname\n ) as detail,\n 'https://supabase.github.io/splinter/0010_security_definer_view' as remediation,\n jsonb_build_object(\n 'schema', n.nspname,\n 'name', c.relname,\n 'type', 'view'\n ) as metadata,\n format(\n 'security_definer_view_%s_%s',\n n.nspname,\n c.relname\n ) as cache_key\nfrom\n pg_catalog.pg_class c\n join pg_catalog.pg_namespace n\n on n.oid = c.relnamespace\nwhere\n c.relkind = 'v'\n and n.nspname = 'public'\n\tand not (\n\t\tlower(coalesce(c.reloptions::text,'{}'))::text[]\n\t\t&& array[\n\t\t\t'security_invoker=1',\n\t\t\t'security_invoker=true',\n\t\t\t'security_invoker=yes',\n\t\t\t'security_invoker=on'\n\t\t]\n\t))",
"0011_function_search_path_mutable": "(\nselect\n 'function_search_path_mutable' as name,\n 'WARN' as level,\n 'EXTERNAL' as facing,\n 'Detects functions with a mutable search_path parameter which could fail to execute sucessfully for some roles.' as description,\n format(\n 'Function \\`%s.%s\\` has a role mutable search_path',\n n.nspname,\n p.proname\n ) as detail,\n 'https://supabase.github.io/splinter/0011_function_search_path_mutable' as remediation,\n jsonb_build_object(\n 'schema', n.nspname,\n 'name', p.proname,\n 'type', 'function'\n ) as metadata,\n format(\n 'function_search_path_mutable_%s_%s_%s',\n n.nspname,\n p.proname,\n md5(p.prosrc) -- required when function is polymorphic\n ) as cache_key\nfrom\n pg_catalog.pg_proc p\n join pg_catalog.pg_namespace n\n on p.pronamespace = n.oid\nwhere\n n.nspname not in (\n 'pg_catalog', 'information_schema', 'auth', 'storage', 'vault', 'pgsodium', 'graphql', 'graphql_public'\n )\n -- Search path not set to ''\n and not coalesce(p.proconfig, '{}') && array['search_path=\"\"'])",
"0012_auth_allow_anonymous_sign_ins": "(\nwith recursive role_members as (\n select\n roleid,\n member\n from pg_catalog.pg_auth_members\n where roleid = (select oid from pg_roles where rolname = 'authenticated')\n union\n select\n am.roleid,\n am.member\n from pg_catalog.pg_auth_members as am\n inner join role_members as rm on am.roleid = rm.member\n),\n\nmember_names as (\n select r.rolname from pg_roles as r\n inner join role_members as m on r.oid = m.member\n)\n\nselect\n 'auth_allow_anonymous_sign_ins' as name,\n 'INFO' as level,\n 'EXTERNAL' as facing,\n 'Detects row level security (RLS) policies that allow access to anonymous users.' as description,\n 'https://supabase.github.io/splinter/0012_auth_allow_anonymous_sign_ins' as remediation,\n format(\n 'Table \\`%s.%s\\` has policies enforced on roles that allow access to anonymous users. Policies include \\`%s\\`',\n n.nspname,\n c.relname,\n array_agg(p.policyname order by p.policyname)\n ) as detail,\n jsonb_build_object(\n 'schema', n.nspname,\n 'name', c.relname,\n 'type', 'table'\n ) as metadata,\n format(\n 'auth_allow_anonymous_sign_ins_%s_%s',\n n.nspname,\n c.relname\n ) as cache_key\nfrom pg_catalog.pg_policies as p\ninner join pg_catalog.pg_class as c on p.tablename = c.relname\ninner join pg_catalog.pg_namespace as n on c.relnamespace = n.oid\nwhere\n (\n p.roles = array['public'::name] -- public roles \n or p.roles = array['authenticated'::name] -- authenticated roles \n or exists (\n select rolname from member_names where rolname = any(roles)\n ) -- roles that are members of authenticated\n )\n and replace(p.qual, ' ', '') not like '%auth.jwt()%->>%is_anonymous%'\ngroup by n.nspname, c.relname)",
"0013_rls_disabled_in_public": "(\nselect\n 'rls_disabled_in_public' as name,\n 'ERROR' as level,\n 'EXTERNAL' as facing,\n 'Detects cases where row level security (RLS) has not been enabled on a table in the `public` schema.' as description,\n format(\n 'Table \\`%s.%s\\` is in the `public` but RLS has not been enabled.',\n n.nspname,\n c.relname\n ) as detail,\n 'https://supabase.github.io/splinter/0013_rls_disabled_in_public' as remediation,\n jsonb_build_object(\n 'schema', n.nspname,\n 'name', c.relname,\n 'type', 'table'\n ) as metadata,\n format(\n 'rls_disabled_in_public_%s_%s',\n n.nspname,\n c.relname\n ) as cache_key\nfrom\n pg_catalog.pg_class c\n join pg_catalog.pg_namespace n\n on c.relnamespace = n.oid\nwhere\n c.relkind = 'r' -- regular tables\n and n.nspname = 'public'\n -- RLS is disabled\n and not c.relrowsecurity)"
}
56 changes: 56 additions & 0 deletions splinter.sql
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,62 @@ where
and not coalesce(p.proconfig, '{}') && array['search_path=""'])
union all
(
with recursive role_members as (
select
roleid,
member
from pg_catalog.pg_auth_members
where roleid = (select oid from pg_roles where rolname = 'authenticated')
union
select
am.roleid,
am.member
from pg_catalog.pg_auth_members as am
inner join role_members as rm on am.roleid = rm.member
),

member_names as (
select r.rolname from pg_roles as r
inner join role_members as m on r.oid = m.member
)

select
'auth_allow_anonymous_sign_ins' as name,
'INFO' as level,
'EXTERNAL' as facing,
'Detects row level security (RLS) policies that allow access to anonymous users.' as description,
'https://supabase.github.io/splinter/0012_auth_allow_anonymous_sign_ins' as remediation,
format(
'Table \`%s.%s\` has policies enforced on roles that allow access to anonymous users. Policies include \`%s\`',
n.nspname,
c.relname,
array_agg(p.policyname order by p.policyname)
) as detail,
jsonb_build_object(
'schema', n.nspname,
'name', c.relname,
'type', 'table'
) as metadata,
format(
'auth_allow_anonymous_sign_ins_%s_%s',
n.nspname,
c.relname
) as cache_key
from pg_catalog.pg_policies as p
inner join pg_catalog.pg_class as c on p.tablename = c.relname
inner join pg_catalog.pg_namespace as n on c.relnamespace = n.oid
where
(
p.roles = array['public'::name] -- public roles
or p.roles = array['authenticated'::name] -- authenticated roles
or exists (
select rolname from member_names where rolname = any(roles)
) -- roles that are members of authenticated
)
and replace(p.qual, ' ', '') not like '%auth.jwt()%->>%is_anonymous%'
group by n.nspname, c.relname)
union all
(
select
'rls_disabled_in_public' as name,
'ERROR' as level,
Expand Down
45 changes: 45 additions & 0 deletions test/expected/0012_auth_allow_anonymous_sign_ins.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
begin;
-- 0 issues
select * from lint."0012_auth_allow_anonymous_sign_ins";
name | level | facing | description | remediation | detail | metadata | cache_key
------+-------+--------+-------------+-------------+--------+----------+-----------
(0 rows)

create table public.documents( id int primary key );
-- Create a policy for the authenticated role that would allow access to anonymous login users
-- if that feature is enabled
create policy "allow_access_to_authenticated" on public.documents
as restrictive
to authenticated
using (true);
-- 1 issues
select * from lint."0012_auth_allow_anonymous_sign_ins";
name | level | facing | description | remediation | detail | metadata | cache_key
-------------------------------+-------+----------+---------------------------------------------------------------------------------+------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------------------------------------------+------------------------------------------------
auth_allow_anonymous_sign_ins | INFO | EXTERNAL | Detects row level security (RLS) policies that allow access to anonymous users. | https://supabase.github.io/splinter/0012_auth_allow_anonymous_sign_ins | Table \`public.documents\` has policies enforced on roles that allow access to anonymous users. Policies include \`{allow_access_to_authenticated}\` | {"name": "documents", "type": "table", "schema": "public"} | auth_allow_anonymous_sign_ins_public_documents
(1 row)

drop policy "allow_access_to_authenticated" on public.documents;
-- Resolve the issue by excluding anonymous login users
create policy "allow_access_to_permanent_users" on documents
as restrictive
to authenticated
using ( (select (auth.jwt() ->> 'is_anonymous')::boolean) is false );
-- 0 issues
select * from lint."0012_auth_allow_anonymous_sign_ins";
name | level | facing | description | remediation | detail | metadata | cache_key
------+-------+--------+-------------+-------------+--------+----------+-----------
(0 rows)

-- Check if policy definition passes with case sensitive characters
create policy "allow_access_to_permanent_users_case_senstive" on documents
as restrictive
to authenticated
using ( (select (AUTH.JWT() ->> 'is_anonymous')::boolean) is false );
-- 0 issues
select * from lint."0012_auth_allow_anonymous_sign_ins";
name | level | facing | description | remediation | detail | metadata | cache_key
------+-------+--------+-------------+-------------+--------+----------+-----------
(0 rows)

rollback;
38 changes: 38 additions & 0 deletions test/sql/0012_auth_allow_anonymous_sign_ins.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
begin;

-- 0 issues
select * from lint."0012_auth_allow_anonymous_sign_ins";

create table public.documents( id int primary key );

-- Create a policy for the authenticated role that would allow access to anonymous login users
-- if that feature is enabled
create policy "allow_access_to_authenticated" on public.documents
as restrictive
to authenticated
using (true);

-- 1 issues
select * from lint."0012_auth_allow_anonymous_sign_ins";

drop policy "allow_access_to_authenticated" on public.documents;

-- Resolve the issue by excluding anonymous login users
create policy "allow_access_to_permanent_users" on documents
as restrictive
to authenticated
using ( (select (auth.jwt() ->> 'is_anonymous')::boolean) is false );

-- 0 issues
select * from lint."0012_auth_allow_anonymous_sign_ins";

-- Check if policy definition passes with case sensitive characters
create policy "allow_access_to_permanent_users_case_senstive" on documents
as restrictive
to authenticated
using ( (select (AUTH.JWT() ->> 'is_anonymous')::boolean) is false );

-- 0 issues
select * from lint."0012_auth_allow_anonymous_sign_ins";

rollback;