DEV Community

OnaEiuspkz
OnaEiuspkz

Posted on

PostgreSQL row security still leaves table owners outside the policy

PostgreSQL row security still leaves table owners outside the policy

Row-level security is often described as a tenant boundary inside PostgreSQL. It becomes that boundary only when the application cannot take one of the roles that PostgreSQL treats as privileged. A table owner normally bypasses row security, and a role with BYPASSRLS bypasses it as well. A policy that works in a test owned by an application account can therefore disappear from the authorization path in production.

PostgreSQL documents the owner exception directly. The table owner can choose to apply policies to itself with ALTER TABLE ... FORCE ROW LEVEL SECURITY. That option closes the owner bypass for ordinary row access, but the owner may still be able to change the table or its policies. If the same compromised application role can issue schema changes, forcing row security does not restore a trustworthy policy boundary.

Separate the roles that own database objects from the roles that serve application queries. The owner can run migrations and policy changes. The application should use a role with the minimum table privileges it needs and no ownership of tenant tables. Check rolsuper and rolbypassrls in pg_roles, and audit role membership for paths that let the application assume a more privileged identity.

Check every policy direction

Policies need to match the operation. A USING expression controls which existing rows are visible or eligible for modification. An INSERT policy needs WITH CHECK, and an UPDATE policy should define both the rows that may be changed and the rows that may result from the change. A tenant policy that only checks tenant_id = current_setting(...) on reads can still allow a write that moves a row into another tenant.

Test with the real connection role. Set the tenant context through a mechanism the connection pool resets reliably. Make the application attempt reads, inserts, updates, and deletes across tenant boundaries. Include an update that changes the tenant column. Confirm both the returned rows and the rows remaining in the table.

Test role escalation

Row security depends on the identity PostgreSQL sees. A pooled connection that can execute SET ROLE or SET SESSION AUTHORIZATION may escape the expected role. Revoke unnecessary role memberships and test the application account against the commands it could invoke through an SQL injection or a privileged administrative feature.

Database extensions, security-definer functions, temporary tables, and views can also change the effective objects and privileges in a query. Review functions that expose tenant tables and make sure their owner and search path are intentional. A security-definer function can be useful, but it should enforce the same tenant rules as the direct table path.

Treat policy tests as authorization tests

A successful migration that creates a policy is not proof that the application cannot cross tenants. Record the connection identity, database, table owner, forced-RLS state, active policies, and role attributes. Run rejection tests as well as allowed tests. A tenant-boundary test suite should fail when a future migration drops FORCE ROW LEVEL SECURITY, changes a policy owner, or grants the application a role with BYPASSRLS.

The practical model has three layers: the connection cannot assume a privileged role, the tables apply policies to their owners, and every operation has a policy that constrains the resulting row. Missing any layer leaves a path that the other two may not cover.

References

Top comments (0)