Postgres CVE-2024-4317 and how to fix the system views

In today’s E114 of “5mins of Postgres” we discuss the updated Postgres minor releases that were just released this week. And specifically, we're going to talk about the security issue that was fixed in this release. Now I'll tell you first off, that this security issue, CVE-2024-4317 is a fairly minor one, but the reason I want to talk about it is, because I was the one who reported it.

I'll explain to you when this can be a problem and how you can apply the fix. If you want to fix this issue, you cannot just upgrade the minor release. You actually do have to take action on an existing database to fix the problem.



Share this episode: Click here to share this episode on LinkedIn or on X/Twitter. Feel free to sign up for our newsletter and subscribe to our YouTube channel.


Transcript

Background on the vulnerability

What is CVE-2024-4317?

The problem tracked under CVE-2024-4317 is an information leak problem between different users on your Postgres system, specifically information that Postgres collected as part of extended statistics or the CREATE STATISTICS command. Further, you'd have to have created statistics on an expression.

The chance that this is actually a problem on your system is unlikely. But especially if you know that you are using extended statistics, this is something that you should certainly investigate and patch.

I discovered this issue when I was working with extended statistics on expressions, specifically in pganalyze we are now tracking extended statistics information. And so as part of that work, I was surprised why we were able to see data that we shouldn't have been able to see.

I want to show you how I reported this issue, how to reproduce it, as well as how to apply the fix.

How to report security issues to Postgres

Before we jump in, an important note if you find yourself in my shoes. When you see security issues like this, you should report them to the security mailing list. Here is a snippet from the email I sent:

from:       Lukas Fittl <lukas@fittl.com>
to:         security@postgresql.org
date:       Nov 19, 2023, 9:47 PM
subject:	Extended statistics: Information leak of MCVs on expressions to
            unprivileged users

Hi all,

Per subject, when working with extended statistics on expressions, I found a missing
access privilege check in the system views pg_stats_ext and pg_stats_ext_exprs, causing
non-superusers to see MCV data from pg_statistic_ext_data they shouldn't have access
to.

For pg_stats_ext the access check as defined in system_views.sql is as follows:

    FROM pg_statistic_ext s JOIN pg_class c ON (c.oid = s.stxrelid)
         JOIN pg_statistic_ext_data sd ON (s.oid = sd.stxoid)
         ...
    WHERE NOT EXISTS
              ( SELECT 1
                FROM unnest(stxkeys) k
                     JOIN pg_attribute a
                          ON (a.attrelid = s.stxrelid AND a.attnum = k)
                WHERE NOT has_column_privilege(c.oid, a.attnum, 'select') )
    AND (c.relrowsecurity = false OR NOT row_security_active(c.oid));

This works correctly for columns referenced directly via stxkeys, but nothing is done
to do the same check for expressions (stxexprs). Thus, when a user has access to all
columns in stxkeys (which may be empty), no further checks are done, and MCV data for
expressions is leaked to unprivileged users.

For pg_stats_ext_exprs there is no access check at all, causing any expression
referenced in an extended statistics object to leak its MCV data.

This appears to have been an oversight when modifying system_views.sql in the commit
that added this (a4d75c86bf15).

...

I got an initial response to this within 48 hours, though the actual fix took longer to be released, mainly due to logistics questions, since patching this kind of issue on released versions is not straightforward.

Also note that you shouldn't tell anybody about it until the Postgres development team has had time to respond. Even on a minor issue like this, I was careful to not talk about it until I knew that minor releases were released that provide a patch.

An example on a virtual machine with 16.2

I'll show that to you first, just to illustrate the problem. So here on my server, this is on a virtual machine that's on the old minor release, this is 16.2.

I'm creating a table, and then I'm inserting some values into it:

CREATE TABLE tbl1(id integer PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, col1 text);
INSERT INTO tbl1(col1) VALUES ('secret'), ('secret'), ('very secret');

And then I'm creating extended statistics objects:

CREATE STATISTICS s_col ON id, col1 FROM tbl1;
CREATE STATISTICS s_expr1 ON id, lower(col1) FROM tbl1;
CREATE STATISTICS s_expr2 ON lower(col1), upper(col1) FROM tbl1;

The first object here is just a regular extended statistics object. And the second and the third one, those are statistics objects on expressions. And so the problem only happens with expression statistics.

Now I'm analyzing this table for the extended statistics to be collected:

ANALYZE tbl1;

And now obviously, if I'm my current user, I'm the super user. I can just access the data:

SELECT * FROM tbl1;
 id |    col1     
----+-------------
  1 | secret
  2 | secret
  3 | very secret
(3 rows)

And I can also, in this case, when I'm the super user, just query the pg_stats_ext view, often useful for debugging, trying to understand which data did Postgres sample:

SELECT statistics_name, most_common_vals FROM pg_stats_ext;
 statistics_name |                most_common_vals                 
-----------------+-------------------------------------------------
 s_col           | {{1,secret},{2,secret},{3,"very secret"}}
 s_expr1         | {{1,secret},{2,secret},{3,"very secret"}}
 s_expr2         | {{secret,SECRET},{"very secret","VERY SECRET"}}
(3 rows)

Postgres, as part of its planner statistics, will collect most common values found in a table. This is then used when a common value shows up in a query, to make smarter planner decisions.

Now, the security issue here happens when you connect to a different user. Here I have my test user, this user does not have privileges to query the table:

SELECT current_user;
 current_user 
--------------
 test
(1 row)

So if I just "SELECT * FROM tbl1", I get permission denied:

SELECT * FROM tbl1;
ERROR:  permission denied for table tbl1

But now the problem is, if I'm running the SELECT from the pg_stats_ext view, I'm able to see the data that I shouldn't be able to see:

SELECT statistics_name, most_common_vals FROM pg_stats_ext;
 statistics_name |                most_common_vals                 
-----------------+-------------------------------------------------
 s_expr2         | {{secret,SECRET},{"very secret","VERY SECRET"}}
(1 row)

This is essentially the core of the issue, even if I'm an unrelated user, I can suddenly see these internal statistics and they may contain sensitive data. Again, it's a little bit narrow, but this clearly can be a problem on some systems.

Upgrading Postgres is not enough!

Now in order to fix this, the community actually published a few extra steps that you need to do to resolve this.

The issue relates to the fact that this vulnerability is in the pg_stats_ext and pg_stats_ext_exprs system views. These system views are defined by Postgres on initialization of the database server with initdb, and are not updated when you apply a minor release.

Now to illustrate this, on this system, we're going to upgrade our Postgres to the new minor release:

apt-get upgrade postgresql-16

We're now on 16.3. This is the fixed version:

SELECT version();
                                                              version                                                              
-----------------------------------------------------------------------------------------------------------------------------------
 PostgreSQL 16.3 (Ubuntu 16.3-1.pgdg22.04+1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0, 64-bit
(1 row)

Now here's the challenging part about this security update. If I now connect here on my other user, this one should not be able to access things. And now I query it again, and this is still broken:

psql -h localhost -U test postgres
psql (16.3 (Ubuntu 16.3-1.pgdg22.04+1))
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off)
Type "help" for help.

postgres=> SELECT statistics_name, most_common_vals FROM pg_stats_ext;
 statistics_name |                most_common_vals                 
-----------------+-------------------------------------------------
 s_expr2         | {{secret,SECRET},{"very secret","VERY SECRET"}}
(1 row)

Even though I just upgraded to the newest minor release, it's still not working as expected because I can query this information, despite having upgraded.

And really the problem comes down to the fact that these views are not updated automatically when you update the release.

How to fix the system views

And so in the community instructions, as noted this fix only fixes fresh Postgres installations, namely those that are created with initdb after the fix is applied, after the new minor release is installed.

Now, if you have a current Postgres installation, like we do here, and we're concerned about this issue, which let's suppose we are, then we need to follow additional remediation steps.

Fix by running the provided psql script

And so we need to do is, we need to essentially run a script that replaces the system view. And you can either do this from the file system if you use the regular packages or you can get this from the Postgres git repository.

The script that applies the fix is literally replacing these views with the fixed version:

SET search_path = pg_catalog;

CREATE OR REPLACE VIEW pg_stats_ext WITH (security_barrier) AS
    ...
    WHERE pg_has_role(c.relowner, 'USAGE')
    AND (c.relrowsecurity = false OR NOT row_security_active(c.oid));

CREATE OR REPLACE VIEW pg_stats_ext_exprs WITH (security_barrier) AS
    ...
    WHERE pg_has_role(c.relowner, 'USAGE')
    AND (c.relrowsecurity = false OR NOT row_security_active(c.oid));

And the relevant part is this WHERE condition here, where the WHERE condition now checks if the user is table owner and only if the user is the table owner or a super-user, then they're allowed to view this information.

Now, here we have the package based install, so we can actually source this SQL file:

postgres=# \i /usr/share/postgresql/16/fix-CVE-2024-4317.sql
SET
CREATE VIEW
CREATE VIEW

We now have the fixed version. With my test user, if I'm trying to access the pg_stats_ext data again, the data is no longer visible:

SELECT statistics_name, most_common_vals FROM pg_stats_ext;
 statistics_name | most_common_vals 
-----------------+------------------
(0 rows)

Confirming the fix is applied

So, just to summarize, if you want to confirm whether you've upgraded correctly or not, you can look at the view definition for "pg_stats_ext", and check this WHERE condition.

Before:

postgres=> \d+ pg_stats_ext
                                    View "pg_catalog.pg_stats_ext"
...
  WHERE NOT (EXISTS ( SELECT 1
           FROM unnest(s.stxkeys) k(k)
             JOIN pg_attribute a ON a.attrelid = s.stxrelid AND a.attnum = k.k
          WHERE NOT has_column_privilege(c.oid, a.attnum, 'select'::text))) AND (c.relrowsecurity = false OR NOT row_security_active(c.oid));
Options: security_barrier=true

After:

postgres=> \d+ pg_stats_ext
                                    View "pg_catalog.pg_stats_ext"
...
  WHERE pg_has_role(c.relowner, 'USAGE'::text) AND (c.relrowsecurity = false OR NOT row_security_active(c.oid));
Options: security_barrier=true

And if you're manually copying the fix, don't forget to include the same fix for pg_stat_ext_exprs.

Applying the fix to all databases

Now, the other thing that we have to do here is we have to apply this on each of our databases. I just fixed this for the "postgres" database, but I actually have the template databases as well:

postgres=# \l
                                                   List of databases
   Name    |  Owner   | Encoding | Locale Provider | Collate |  Ctype  | ICU Locale | ICU Rules |   Access privileges   
-----------+----------+----------+-----------------+---------+---------+------------+-----------+-----------------------
 postgres  | postgres | UTF8     | libc            | C.UTF-8 | C.UTF-8 |            |           | 
 template0 | postgres | UTF8     | libc            | C.UTF-8 | C.UTF-8 |            |           | =c/postgres          +
           |          |          |                 |         |         |            |           | postgres=CTc/postgres
 template1 | postgres | UTF8     | libc            | C.UTF-8 | C.UTF-8 |            |           | =c/postgres          +
           |          |          |                 |         |         |            |           | postgres=CTc/postgres
(3 rows)

And the template databases are important because those databases are the ones that are essentially used for instantiating new databases.

So if I were to run CREATE DATABASE now, I would actually have the vulnerability happen again. And so what I need to do now is for template0, I temporarily need to allow connections:

ALTER DATABASE template0 WITH ALLOW_CONNECTIONS true;

I can connect to it, and apply the fix:

postgres=# \c template0
You are now connected to database "template0" as user "postgres".
template0=# \i /usr/share/postgresql/16/fix-CVE-2024-4317.sql
SET
CREATE VIEW
CREATE VIEW

And then I can connect to template1, do the same thing:

template0=# \c template1
You are now connected to database "template1" as user "postgres".
template1=# \i /usr/share/postgresql/16/fix-CVE-2024-4317.sql
SET
CREATE VIEW
CREATE VIEW

And then I make template0 to not allow connections again:

ALTER DATABASE template0 WITH ALLOW_CONNECTIONS false;

This now is a complete fix. Now we actually know that both existing databases and new databases no longer have this security vulnerability.

In conclusion

CVE-2024-4317 is not a problem for all installations, but if you use extended statistics its a good idea to apply the fixes described in this episode.

I hope you learned something new from E114 of 5mins of Postgres. Feel free to subscribe to our YouTube channel, sign up for our newsletter or follow us on LinkedIn and X/Twitter to get updates about new episodes!

What we have discussed in this episode of 5mins of Postgres


Enjoy blog posts like this?

Get them once a month to your inbox