Postgres's Row-Level Security (RLS) is a powerful feature for implementing fine-grained access control, but it's riddled with subtle traps that can destroy performance or completely bypass security. This comprehensive guide covers all the major footguns with practical fixes and real-world examples.
1. The LEAKPROOF Function Performance Killer#
The footgun: Non-LEAKPROOF functions in RLS policies prevent index usage, causing catastrophic performance degradation.
Why it happens: Postgres must apply RLS filtering first, then non-LEAKPROOF functions, preventing the query planner from using indexes.
Example problem:
The fix:
Performance impact: Queries can go from milliseconds to hours on large tables.
2. Complex Policy Performance Death#
The footgun: Complex RLS policies with subqueries execute for every row, multiplying query cost exponentially.
Bad example:
Better approach:
3. Missing Indexes on Policy Columns#
The footgun: Forgetting to index columns used in RLS policies forces sequential scans.
Essential indexes:
4. The BYPASSRLS Superuser Trap#
The footgun: Superusers and table owners bypass RLS by default, creating false security confidence during testing.
Why it's dangerous: Testing with superuser accounts makes RLS appear to work when it's actually being ignored.
The fix:
Testing pattern:
5. SECURITY DEFINER View Bypass#
The footgun: Views are SECURITY DEFINER by default, running with creator's privileges and bypassing RLS.
Dangerous example:
Secure approaches:
6. Timing Side-Channel Attacks#
The footgun: Query execution time leaks information about restricted data, allowing sophisticated attacks.
Attack scenario: In a multi-tenant medical database, an attacker measures query times to determine if patients with specific conditions exist in other tenants' data.
Technical details:
- RLS policy enforcement creates measurable timing differences
- Attackers can infer secret cardinality information
- Works even across network latency in cloud environments
Example vulnerable query:
Mitigation strategies:
- Use data-oblivious query patterns (performance cost)
- Add artificial delays to normalize timing
- Limit query complexity for untrusted users
- Monitor for suspicious timing-based query patterns
Research note: This attack has been demonstrated in academic research and works in real-world cloud environments.
7. CVE-2019-10130: Statistics Leakage#
The footgun: CVE-2019-10130. Postgres's query planner statistics could leak sampled data from RLS-protected rows.
Technical details:
- Query planner collects statistics by sampling column data
- Users could craft operators to read statistics containing forbidden data
- Affected Postgres 9.5-11 before May 2019 patches
Status: Fixed in Postgres 9.5.17, 9.6.13, 10.8, 11.3
Lesson: Keep Postgres updated and remember that even internal mechanisms can leak data.
8. Missing FORCE ROW LEVEL SECURITY#
The footgun: Enabling RLS without FORCE allows table owners to bypass policies.
Problem:
Solution:
9. USING vs WITH CHECK Confusion#
The footgun: USING filters existing rows for SELECT/UPDATE/DELETE, but WITH CHECK validates new/modified rows for INSERT/UPDATE.
Dangerous example:
Correct approach:
10. Connection Pooling Context Loss#
The footgun: With connection pooling, current_user is a shared database role, useless for tenant isolation.
Problem:
Solution:
Security hardening:
11. Foreign Key Failures Under RLS#
The footgun: INSERT into child tables fails FK checks because RLS blocks SELECT on parent rows.
Example failure:
Solution:
12. Unique Constraint Cross-Tenant Leakage#
The footgun: Global unique constraints reveal data existence across tenants.
Problem:
Solution:
13. Silent Failures#
The footgun: RLS failures are silent - operations fail without errors or warnings.
Example: An UPDATE that should modify 100 rows silently affects 0 rows due to RLS policy.
Debugging approach:
14. Column-Level Security Gaps#
The footgun: RLS filters rows, not columns. Sensitive columns remain visible in allowed rows.
Problem:
Solutions:
15. Materialized Views and Background Jobs#
The footgun: Data copied to materialized views or exported by jobs isn't automatically protected by source table policies.
Problems:
Solutions:
16. Multiple Policy Confusion#
The footgun: Multiple permissive policies are OR-ed together; one broad policy can override stricter ones.
Problem:
Solutions:
Key Takeaways#
- RLS is not a security silver bullet - it can be bypassed through multiple vectors
- Performance impact is severe - always index policy columns and keep policies simple
- Testing methodology is critical - never test with superuser accounts
- Silent failures make debugging painful - policies fail without warnings
- Context management is crucial - use secure session variables, not current_user
- Defense in depth - combine RLS with column privileges, secure views, and application-level controls
RLS is powerful when implemented correctly, but requires careful attention to these footguns to avoid catastrophic security and performance failures.