Audit PostgreSQL RLS Bypass Roles
Find roles and execution paths that can read past PostgreSQL row security.
A correct RLS policy can still be bypassed by the wrong database role. PostgreSQL states that superusers and roles with BYPASSRLS always bypass row security; table owners normally bypass it unless FORCE ROW LEVEL SECURITY is set. A pooled application connection, migration script, or support console using one of those roles can defeat the intended tenant boundary.
Create a bypass inventory
List every credential that reaches tenant tables. Do not put password values in the inventory. Record the role attributes, owned tables, how the credential is stored, and which application path uses it.
| Path | Database role | Bypass reason | Required control |
|---|---|---|---|
| User API | Restricted app role | None expected | RLS policy tests |
| Background export | Service role | May need broad reads | Explicit tenant-bound query |
| Migration | Owner role | Schema work | Time-bound use and audit |
| Support console | Privileged role | Exceptional access | Approval and session record |
Check connection strings and deployment configuration without printing secrets. Compare the roles you found with the role used by the RLS test suite. A test run only as a restricted role says nothing about a production worker that uses a table owner.
Where a bypass is necessary, write the compensating rule at the actual access point. For example, an export worker can load a job's verified tenant context and use an owner-bound query. Test it with two synthetic tenants, including a missing tenant value. The safe result for missing context is a stop, not an unfiltered query.
Check deployed roles and table ownership
First, run this read-only catalog query against the database that serves the application. Replace the schema name with the schema that holds tenant rows:
SELECT n.nspname AS schema_name, c.relname AS table_name,
pg_get_userbyid(c.relowner) AS table_owner,
c.relrowsecurity AS rls_enabled,
c.relforcerowsecurity AS force_rls
FROM pg_class AS c
JOIN pg_namespace AS n ON n.oid = c.relnamespace
WHERE n.nspname = 'app' AND c.relkind IN ('r', 'p')
ORDER BY 1, 2;
Then inspect only the roles used by the API, workers, migration job, and support tools:
SELECT rolname, rolsuper, rolbypassrls
FROM pg_roles
WHERE rolname IN ('app_role', 'export_role', 'migration_role');
The PostgreSQL role view exposes these attributes without password values. The table catalog exposes RLS and FORCE RLS flags. Replace the example role names with roles observed in deployment configuration. For each connection path, record current_user in a controlled test and compare it with the table owner and role flags. A configuration label alone can differ from the role active after a role switch. Also inspect role memberships and security-definer functions on paths that reach these tables. Record only role attributes and workload owners; do not expose connection strings.
The PostgreSQL documentation is the authority for role behavior. The OWASP Multi Tenant Security Cheat Sheet explains why shared resources need independent controls. This inventory is not an argument to remove privileged roles blindly. It is a map of where the database policy does not apply and who owns the remaining check. Hold an isolation claim if any production access path has unknown role attributes.
Find the gaps in your access controls
See how an illustrative cross-workspace finding connects evidence, impact and a fix. Discuss the tenant boundaries your team needs reviewed.
Sources
- PostgreSQL Row Security PoliciesAccessed
- OWASP Multi Tenant Security Cheat SheetAccessed
- PostgreSQL pg_roles ViewAccessed
- PostgreSQL pg_class CatalogAccessed