From ef306bd555260572e5dcb6728af0bcf39dd64836 Mon Sep 17 00:00:00 2001 From: Anton Karpov Date: Sun, 2 Aug 2026 22:53:06 +0300 Subject: [PATCH] fix(0002): constrain pg_depend classid/refclassid to avoid cross-catalog oid collisions `0002_auth_users_exposed` joined pg_depend to auth.users on refobjid alone: join pg_catalog.pg_depend d on d.refobjid = auth_users_pg_class.oid pg_depend.objid/refobjid are only unique within a (classid, refclassid), so a dependency row that points at an object in a different catalog whose oid happens to equal auth.users' pg_class oid matches as well. In the reported case a view called a SECURITY DEFINER function whose pg_proc oid equalled auth.users' pg_class oid (16499); the view selects only from public tables, yet it was reported as CRITICAL auth_users_exposed and triggered a security advisory email. Add `d.refclassid = 'pg_catalog.pg_class'::regclass` and `d.classid = 'pg_catalog.pg_rewrite'::regclass`, the same filters the other lints already carry after #166. Also drop the redundant `pg_class pg_class_auth_users` join. Once refclassid is constrained it always resolves to the same row as `auth_users_pg_class`, which is already restricted to auth.users. The regression test forces the collision by moving a function's pg_proc oid onto auth.users' pg_class oid inside the test transaction, then asserts the view that calls it is not reported. splinter.sql regenerated via bin/compile.py. Closes #171 --- lints/0002_auth_users_exposed.sql | 8 ++++--- splinter.sql | 8 ++++--- test/expected/0002_auth_users_exposed.out | 26 +++++++++++++++++++++++ test/sql/0002_auth_users_exposed.sql | 26 +++++++++++++++++++++++ 4 files changed, 62 insertions(+), 6 deletions(-) diff --git a/lints/0002_auth_users_exposed.sql b/lints/0002_auth_users_exposed.sql index b903e71..7541abe 100644 --- a/lints/0002_auth_users_exposed.sql +++ b/lints/0002_auth_users_exposed.sql @@ -27,16 +27,18 @@ from and auth_users_pg_class.relname = 'users' and auth_users_pg_namespace.nspname = 'auth' -- Depends on auth.users + -- objid/refobjid are only unique within a (class, refclass), so both must + -- be constrained or a numerically equal oid from another catalog matches join pg_catalog.pg_depend d on d.refobjid = auth_users_pg_class.oid + and d.refclassid = 'pg_catalog.pg_class'::regclass + and d.classid = 'pg_catalog.pg_rewrite'::regclass join pg_catalog.pg_rewrite r on r.oid = d.objid join pg_catalog.pg_class c on c.oid = r.ev_class join pg_catalog.pg_namespace n on n.oid = c.relnamespace - join pg_catalog.pg_class pg_class_auth_users - on d.refobjid = pg_class_auth_users.oid where d.deptype = 'n' and ( @@ -80,7 +82,7 @@ where 'security_invoker=on' ] ) - and not pg_class_auth_users.relrowsecurity + and not auth_users_pg_class.relrowsecurity ) ) group by diff --git a/splinter.sql b/splinter.sql index 9cd66c9..bae1d4e 100644 --- a/splinter.sql +++ b/splinter.sql @@ -127,16 +127,18 @@ from and auth_users_pg_class.relname = 'users' and auth_users_pg_namespace.nspname = 'auth' -- Depends on auth.users + -- objid/refobjid are only unique within a (class, refclass), so both must + -- be constrained or a numerically equal oid from another catalog matches join pg_catalog.pg_depend d on d.refobjid = auth_users_pg_class.oid + and d.refclassid = 'pg_catalog.pg_class'::regclass + and d.classid = 'pg_catalog.pg_rewrite'::regclass join pg_catalog.pg_rewrite r on r.oid = d.objid join pg_catalog.pg_class c on c.oid = r.ev_class join pg_catalog.pg_namespace n on n.oid = c.relnamespace - join pg_catalog.pg_class pg_class_auth_users - on d.refobjid = pg_class_auth_users.oid where d.deptype = 'n' and ( @@ -180,7 +182,7 @@ where 'security_invoker=on' ] ) - and not pg_class_auth_users.relrowsecurity + and not auth_users_pg_class.relrowsecurity ) ) group by diff --git a/test/expected/0002_auth_users_exposed.out b/test/expected/0002_auth_users_exposed.out index 066c82d..52e0e64 100644 --- a/test/expected/0002_auth_users_exposed.out +++ b/test/expected/0002_auth_users_exposed.out @@ -44,6 +44,32 @@ begin; select * from lint."0002_auth_users_exposed"; name | title | level | facing | categories | description | detail | remediation | metadata | cache_key ------+-------+-------+--------+------------+-------------+--------+-------------+----------+----------- +(0 rows) + + rollback to savepoint a; + -- Not a failure mode: a view depending on an object from another catalog + -- whose oid numerically collides with auth.users' pg_class oid. + -- pg_depend.refobjid is only unique within a refclassid, so without a + -- refclassid filter such a view is reported even though it never + -- references auth.users. + create table public.orders (id int primary key); + create function public.order_count() returns bigint language sql as + $$select pg_catalog.count(*) from public.orders$$; + -- move the function's pg_proc oid onto auth.users' pg_class oid + update pg_catalog.pg_proc + set oid = ( + select c.oid + from pg_catalog.pg_class c + join pg_catalog.pg_namespace n + on n.oid = c.relnamespace + where n.nspname = 'auth' and c.relname = 'users' + ) + where oid = 'public.order_count'::pg_catalog.regproc; + create view public.order_stats as select public.order_count() as total; + -- 0 entries + select * from lint."0002_auth_users_exposed"; + name | title | level | facing | categories | description | detail | remediation | metadata | cache_key +------+-------+-------+--------+------------+-------------+--------+-------------+----------+----------- (0 rows) rollback; diff --git a/test/sql/0002_auth_users_exposed.sql b/test/sql/0002_auth_users_exposed.sql index 19abaaa..b245381 100644 --- a/test/sql/0002_auth_users_exposed.sql +++ b/test/sql/0002_auth_users_exposed.sql @@ -37,4 +37,30 @@ begin; select * from lint."0002_auth_users_exposed"; + rollback to savepoint a; + + + -- Not a failure mode: a view depending on an object from another catalog + -- whose oid numerically collides with auth.users' pg_class oid. + -- pg_depend.refobjid is only unique within a refclassid, so without a + -- refclassid filter such a view is reported even though it never + -- references auth.users. + create table public.orders (id int primary key); + create function public.order_count() returns bigint language sql as + $$select pg_catalog.count(*) from public.orders$$; + -- move the function's pg_proc oid onto auth.users' pg_class oid + update pg_catalog.pg_proc + set oid = ( + select c.oid + from pg_catalog.pg_class c + join pg_catalog.pg_namespace n + on n.oid = c.relnamespace + where n.nspname = 'auth' and c.relname = 'users' + ) + where oid = 'public.order_count'::pg_catalog.regproc; + create view public.order_stats as select public.order_count() as total; + -- 0 entries + select * from lint."0002_auth_users_exposed"; + + rollback;