Skip to content

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.

PathDatabase roleBypass reasonRequired control
User APIRestricted app roleNone expectedRLS policy tests
Background exportService roleMay need broad readsExplicit tenant-bound query
MigrationOwner roleSchema workTime-bound use and audit
Support consolePrivileged roleExceptional accessApproval 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

  1. PostgreSQL Row Security PoliciesAccessed
  2. OWASP Multi Tenant Security Cheat SheetAccessed
  3. PostgreSQL pg_roles ViewAccessed
  4. PostgreSQL pg_class CatalogAccessed