Row-Level Security Enforcement in PostgreSQL-Backed Applications
Database policies alone cannot protect against application logic that uses the wrong tenant context.

PostgreSQL’s Row-Level Security puts tenant isolation in the database, so leaving out an application filter cannot skip the check. But that guarantee depends on which role runs a query, which objects it passes through, and whether the engine applies the right policy. Correct RLS means more than writing rules. It means proving those rules fire when the application runs, while testing the API boundary that the rules cannot protect.
Why RLS exists and what guarantee it actually provides
With application-layer filtering, every query has to keep each tenant’s data separate. A missing WHERE clause, an ORM abstraction that drops a filter, or a rushed code path can expose another tenant’s rows without an error or log entry. The query itself can still be valid. Its result is simply broader than the user should see.
RLS moves that check into PostgreSQL itself. When policy enforcement applies, the engine evaluates the policy before returning results, so a query that forgets to filter by tenant_id still cannot return forbidden rows. For multi-tenant SaaS, that changes the architecture of isolation rather than merely improving query-writing discipline.
The default exemptions that make a naively configured RLS implementation silently broken
The engine does not enforce RLS for every role. By default, table owners are exempt, superusers bypass policies, and roles granted BYPASSRLS skip all policy checks. A protected table can therefore return unrestricted results through a connection that never invokes its row policies.
That creates a dangerous artifact in development and testing. Developers connecting as the table owner or a superuser can see expected results and conclude that RLS works, although the policies were never evaluated. A successful query proves very little on its own. It may show only that a privileged connection can read the table.
To remove the table-owner exemption, set FORCE ROW LEVEL SECURITY on each table. That does not remove the superuser or BYPASSRLS exemptions. The production application should use a low-privilege role that neither owns the tables nor has either bypass privilege. SET ROLE and SET SESSION AUTHORIZATION also need scrutiny because a session can switch into a more privileged context without a visible failure.
How SECURITY DEFINER functions and views create bypass paths that survive correct policy configuration
A SECURITY DEFINER function executes with its owner’s privileges. If that owner is exempt from RLS, queries inside the function can bypass policies even when the application session would be restricted. Forcing RLS removes the table-owner exemption, but it does not constrain a superuser-owned function in the same way.
Views add another layer between the caller and the table. A view created by a privileged role may run its underlying query under that role’s privileges, bypassing policies that the application role would otherwise encounter. Materialized views also need review as objects exposing tenant-scoped data.
These paths produce no automatic warning that enforcement was skipped. The remediation is to audit every SECURITY DEFINER function that touches protected tables and verify ownership of views exposing tenant data. Correct policies cannot compensate for an exempt execution context. So far, these failures come from configuration and design, but the engine itself can also apply the wrong policy.
CVE-2026-14666: how plan cache staleness turns a privilege revocation into a persistent access window
PostgreSQL’s advisory for CVE-2026-14666 describes incomplete tracking of changes to role membership, role attributes, and database ownership. When a query reuses a plan, it can keep using cached row-level security policies after a change calls for a different policy. The fix was published on 2026-08-13. The flaw concerns invalidation of cached policy decisions.
Revocation therefore may not take effect for an existing cached plan. Stale policies persist until another event invalidates the cache or the connection ends. During that window, a user can complete reads and modifications that were previously allowed but are now forbidden. An access change alone does not necessarily close the window.
The attack requires tailoring to an application’s privilege-removal pattern and role-specific policies. That limits how broadly the flaw can be exploited. Still, tenant offboarding, role demotions, and subscription downgrades make privilege changes ordinary SaaS operations.
PostgreSQL’s advisory lists fixed releases as 18.6, 17.11, 16.15, 15.19, and 14.24. Teams should compare their exact release with the fix for its branch. NVD describes CVE-2024-10976 as incomplete table tracking that lets reused queries view or change unintended rows, potentially permitting otherwise-forbidden reads and modifications.
What correct RLS policy construction looks like in practice
The practical baseline starts with each tenant-scoped table. Enable RLS explicitly with ALTER TABLE ... ENABLE ROW LEVEL SECURITY, then set FORCE ROW LEVEL SECURITY on that table as well. Turning on RLS creates no policies, so it starts by denying all access to roles that must follow its rules. Treat enable and force as a pair. Applying them to one table does not configure the others.
Next, build policies around current_setting('app.current_tenant_id') or a session-local variable that serves the same purpose and is set before queries run. The policy should use that context rather than trust a tenant ID supplied in the query itself. The predicate belongs under the database’s control. As the API boundary makes clear, however, the application still has to establish trustworthy tenant context.
Separate SELECT, INSERT, UPDATE, and DELETE policies where their access rules differ. A permissive policy covering every operation can over-permit writes. Grant the application role only the privileges needed to query the protected tables, without table ownership or BYPASSRLS. Finally, review functions that touch those tables and, where possible, replace SECURITY DEFINER with SECURITY INVOKER so they run under the caller’s policy-enforced role.
How to verify that RLS policies actually fire under real application conditions
Verification must use the application role. Create at least two tenants and execute the same query under each tenant’s session context. Check both the returned rows and their tenant ownership. Neither session should return the other tenant’s data, even when the query omits its own tenant filter.
Next, query a tenant-scoped table without app.current_tenant_id or its equivalent set. The required isolation outcome is zero returned rows. Test each SECURITY DEFINER function explicitly as the application role. Its row count and content should match what the caller’s policy permits.
Privilege changes need their own operational safeguard while affected instances remain unpatched. A fresh connection closes the stale-session access window described by the CVE. This mitigation does not replace installing the fixed release.
Traditional automated vulnerability scanners usually work within a single authenticated session with their default settings, limiting their results to one tenant context. Database-level cross-tenant testing must be done by hand or with a multi-tenant harness built for the task, so independent verification needs more than scanner output.
Why application-layer IDOR remains the highest-impact risk even when RLS is configured correctly
RLS cannot decide whether an API chose the right tenant context. An endpoint may accept a record ID, tenant ID, or document UUID and fetch the resource using the application’s database credentials. If it sets the session variable from caller-supplied input without validating ownership, the database can enforce the policy perfectly for the wrong tenant. The policy works, but the authorization decision fails.
This is the core danger of an insecure direct object reference, or IDOR. The API accepts an external identifier without establishing that the requesting user may access it. Because tenants share the same code paths, a flaw that reaches one tenant’s data can, by the same logic, reach every tenant’s data.
Correct RLS therefore remains valuable without being sufficient. Treating it as complete tenant isolation can reduce attention at the API layer, precisely where a caller can influence the context handed to the database. This is a logic flaw. The fix is to check ownership on every request that accepts an external identifier and test those checks across multiple live tenant accounts.
What a pen test of RLS and tenant isolation needs to cover to produce credible findings
A credible RLS assessment needs whitebox access to the application’s enforcement design. Testers need to know which tables have RLS, which roles the application uses, which functions are SECURITY DEFINER, and which session variables hold tenant context. Source code, cloud configuration, and architecture documentation make that mapping possible. Greybox or blackbox testing cannot provide equivalent coverage of the database configuration surface.
At the database layer, the test must cover every tenant-scoped table. It should establish that RLS is enabled and forced, and that the application role neither owns the tables nor has BYPASSRLS. Function bypasses need direct testing as that role. Missing-context queries must return no rows, and the installed PostgreSQL release must be compared with the CVE’s fixed release for its branch.
At the API layer, the test needs at least two tenant accounts and a user for each meaningful role in each tenant. Every endpoint accepting an external identifier should be exercised by replaying a request with the second tenant’s identifier. Responses must not return or change that tenant’s data. The test must also establish that tenant session context comes from a trusted authenticated session.
Findings need evidence that an engineering team can act on. A claimed cross-tenant isolation failure should include a working exploit demonstrating actual row or record access. For compliance use, the engagement should cover systems within the SOC 2 or ISO 27001 boundary and follow a recognized methodology such as PTES, NIST SP 800-115, or OWASP WSTG. A vague account of the methodology can lead auditors to ask more questions and hold up certification.
Keeping the enforcement guarantee intact after the initial test: retesting, patching, and continuous scanning
A pen test proves only what held during the engagement. A new tenant-scoped table, API endpoint, or SECURITY DEFINER function changes part of the tested surface. Unpatched PostgreSQL instances introduce a separate risk even when application configuration stays correct.
Remediation therefore needs proof after the fix reaches production. PCI DSS 11.4.4 explicitly requires confirmation through retesting, while SOC 2 auditors increasingly treat reports without a retest or remediation attestation as incomplete evidence. Closing a ticket does not demonstrate that access is blocked. The test must show that the reported failure no longer works.
Scanning pull requests as they come in can catch certain changes that weaken isolation before they are deployed. Those checks should flag tenant-scoped tables missing FORCE ROW LEVEL SECURITY, new SECURITY DEFINER functions touching protected tables, and endpoints accepting external identifiers without ownership validation. They complement multi-tenant testing. Testing only once a year leaves nearly a full year of changes between reviews.
The lasting goal is an enforcement configuration that can be re-verified on demand. That requires auditable role privileges, a current PostgreSQL release for the branch, and review of significant changes for new bypass paths before merge. Database policies and API ownership checks need evidence together.


