Summary:
CVE-2024-10976 is a newly-identified vulnerability in PostgreSQL that can let a user bypass row-level security (RLS) and access or modify rows that should be off-limits. The bug stems from how PostgreSQL reuses query plans whenever RLS is active alongside certain SQL features such as subqueries, CTEs (WITH), security-invoker views, or SQL-language functions. This creates a scenario where a query planned by one user may be executed by another, possibly with very different privilege levels. As a result, strict policies enforced by RLS can be silently subverted, leading to data leakage or unauthorized writes.

This vulnerability builds on the problems first flagged by CVE-2023-2455 and CVE-2016-2193, covering missed edge cases that could have been exploited even after those CVEs were fixed. Any PostgreSQL database using CREATE POLICY for row-level security (introduced in PostgreSQL 9.5) remains potentially exposed until upgraded.

Who is Affected?
Any application running a PostgreSQL version:

12.20 and below

You are only affected if you are using RLS policies with CREATE POLICY and have situations where queries might be planned and executed by different users due to SET ROLE, security definer/invoker functions, or certain query features.

Background: How Row-Level Security in PostgreSQL Works

PostgreSQL’s RLS lets database admins set up fine-grained rules specifying which rows each user is allowed to see or modify in a given table. These rules are especially useful in multi-tenant SaaS apps, HR systems, or anything dealing with sensitive data, because they act as an extra line of defense.

-- Example: RLS policy allowing each user to see only their own rows
ALTER TABLE confidential ADD POLICY my_data_policy
    USING (owner_id = current_user);

The Vulnerability: Incomplete Query Tracking Lets Policy Get Mixed Up

When PostgreSQL runs a query, it usually tries to avoid doing all the expensive SQL parsing and planning each time, especially for common queries—improving its performance using what’s called a "query plan cache." If a query is reused (say, by executing a prepared statement, or in a function), PostgreSQL may regenerate the plan less often than you’d think.

When a query involves RLS, PostgreSQL should always make sure that the plan for each query reflects the active user and current policies. CVE-2024-10976 shows that, in some cases, especially with subqueries, CTEs, security-invoker views, or SQL-language functions, PostgreSQL's tracking is incomplete.

What does this mean?
An attacker—or even an honest user, accidentally—may trigger a situation where a query plan, created for one user (maybe with lots of permissions), gets reused when the same SQL is executed under another user (with fewer or different permissions).

Now, RLS policies might not be reapplied as expected. As a result, the query could show or let updates to rows that should be hidden or forbidden.

A realistic attack considered by PostgreSQL’s team would go as follows

1. An app or user creates a prepared statement, view, or SQL function under one role (with high privileges).
2. Later, another user executes the ORM function or query after switching roles (SET ROLE), or within the context of a security-invoker view/function.
3. Query is reused, but with originally planned RLS policies (for the privileged role), not re-planned for the new restricted user.
4. Result: Rows normally hidden from the restricted user are now visible or modifiable, since the RLS filter is bypassed.

Exploit Example: Seeing Another User's Data

Let’s see a simplified sketch of an exploit scenario.

Suppose you have this table and policy

CREATE TABLE documents (
    id serial PRIMARY KEY,
    owner text,
    content text
);

ALTER TABLE documents ENABLE ROW LEVEL SECURITY;

CREATE POLICY docs_by_owner
    ON documents
    USING (owner = current_user);

Two users: alice and bob. Each should only see their own docs.

Now, consider the following SQL function

CREATE OR REPLACE FUNCTION get_documents()
RETURNS SETOF documents
LANGUAGE SQL
SECURITY INVOKER
AS $$
    SELECT * FROM documents;
$$;

Assume alice executes this first (so a query plan is created for *her* permissions)

SET ROLE alice;
SELECT * FROM get_documents();
-- Alice gets only her docs, as expected.

Next, an administrator does not clear the plan cache. Now bob does

SET ROLE bob;
SELECT * FROM get_documents();
-- Instead of getting just Bob's docs, Bob might now see Alice's,
-- if the cached plan is reused!

This is a trivial example, but real-world, attackers would need to tailor this approach to your application's actual use of prepared statements, views, and role switching.

Responsible Disclosure and Patch

The PostgreSQL team has released patched versions to address CVE-2024-10976, with changes ensuring that query plans involving RLS get invalidated or regenerated upon role change, especially for the SQL cases listed above.

If you use RLS, upgrade ASAP to:

12.21

Official Release Note:
- PostgreSQL Security Release—Feb 2024

More Details:
- CVE record at NIST NVD
- Official PostgreSQL Announcement

Mitigation

Short-term:
- Audit application code for SET ROLE, security-definer/invoker functions, role-specific RLS, and heavy reuse of prepared statements.

Avoid using shared prepared statements across multiple roles for RLS-protected data.

Long-term:

Upgrade PostgreSQL to a patched version.

- Consider adding application-level tests to verify that unauthorized users cannot access other users’ data due to query cache surprises.

Conclusion

CVE-2024-10976 shines a light on how tricky defending against privilege escalation bugs can be, especially in powerful and flexible systems like PostgreSQL. While most applications may not be at risk, the potential for RLS bypass bugs in sensitive systems makes immediate patching critical for any production database using row-level security.

Staying up-to-date with patches and having strong audit controls for cross-user and cross-role query execution paths remains a core part of secure database operations.


For more technical details and the full commit fix, see:
- Commit fixing CVE-2024-10976


#### _Keep your PostgreSQL safe—patch early, patch often, and always validate your security controls for unexpected edge cases!_

Timeline

Published on: 11/14/2024 13:00:01 UTC