Changes to the public schema in PostgreSQL 15 and how to handle upgrades
Posted by
ads
on
Tuesday, 2022-08-30 Posted in [Postgresql-News]
In September 2021, a patch for the upcoming PostgreSQL version 15 was committed which introduces a visible change for users: the CREATE privilege for the public schema is no longer set by default. This is a recommendation from CVE-2018-1058.
What does that mean for the average (non-superuser) user?
postgres=#SELECTversion();version-------------------------------------------------------------------------------------------------------
PostgreSQL 14.5on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 9.4.0-1ubuntu1~20.04.1)9.4.0,64-bit(1row)postgres=#CREATEROLE unprivileged WITH LOGIN;CREATEROLEpostgres=#CREATEDATABASE priv_test;CREATEDATABASEpostgres=#\c priv_test
You are now connected todatabase"priv_test"asuser"ads".priv_test=#\dn+ public
List of schemas
Name|Owner|Accessprivileges| Description
--------+-------+-------------------+------------------------
public | ads | ads=UC/ads +| standard public schema||=UC/ads |(1row)
We see that PUBLIC (the second line in Access privileges) has USAGE (U) and CREATE (C) privileges. A regular user can create a table in the public schema:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
priv_test=#SETSESSIONROLE unprivileged;SETpriv_test=>SHOW search_path; search_path
-----------------
"$user", public
(1row)priv_test=>CREATETABLE priv_test (id INT);CREATETABLEpriv_test=>\dp priv_test
AccessprivilegesSchema|Name|Type|Accessprivileges|Columnprivileges| Policies
--------+-----------+-------+-------------------+-------------------+----------
public | priv_test |table|||(1row)
This is how the public schema privileges look in PostgreSQL 15:
postgres=#SELECTversion();version----------------------------------------------------------------------------------------------------------
PostgreSQL 15beta3 on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 9.4.0-1ubuntu1~20.04.1)9.4.0,64-bit(1row)postgres=#CREATEROLE unprivileged WITH LOGIN;CREATEROLEpostgres=#CREATEDATABASE priv_test;CREATEDATABASEpostgres=#\c priv_test
You are now connected todatabase"priv_test"asuser"ads".priv_test=#\dn+ public
List of schemas
Name|Owner|Accessprivileges| Description
--------+-------------------+----------------------------------------+------------------------
public | pg_database_owner | pg_database_owner=UC/pg_database_owner+| standard public schema||=U/pg_database_owner |(1row)
The C (CREATE) is missing. And the CREATE TABLE for a regular user is no longer working by default:
1
2
3
4
5
6
7
8
9
10
11
priv_test=#SETSESSIONROLE unprivileged;SETpriv_test=>SHOW search_path; search_path
-----------------
"$user", public
(1row)priv_test=>CREATETABLE priv_test (id INT);ERROR: permission denied forschema public
LINE1:CREATETABLE priv_test (id INT);
But how is this change handled in upgrades?
There are two ways to do upgrades. Ok, there are a few more ways, like reinstalling your application. However for the purpose of this article I’m only looking at ways which transfer over the data from the old into the new version.
Dump the data from the old version (14 and older), and restore into the new version (15 and newer)
Both ways work a bit different. Let’s look at the details.
Dump & Restore
Dumping a database is done using the pg_dump tool which comes with PostgreSQL. Preferably using either the custom or the directory format. And one needs to use the pg_dump from the newer version (15) to dump the old database:
1
/path/to/15/bin/pg_dump -F c -f /tmp/backup.dump priv_test
The restore is done by creating an empty database (use template0 as source/template), and then restore the dump into the new database, using pg_restore.
priv_test=#\dn+ public
List of schemas
Name|Owner|Accessprivileges| Description
--------+-------------------+----------------------------------------+------------------------
public | pg_database_owner | pg_database_owner=UC/pg_database_owner+| standard public schema||=U/pg_database_owner |(1row)
Why does the public schema have the “new” privileges, where CREATE is missing?
Because the public schema is included in template0, and is copied into the newly created database. And in PostgreSQL 15, the public schema in template0 has the “new” privileges. Let’s have a look (don’t do this in your production system):
priv_test=#SELECT datname, datallowconn FROM pg_database; datname | datallowconn
-----------+--------------
postgres | t
template1 | t
template0 | f
priv_test | t
(4rows)priv_test=#UPDATE pg_database SET datallowconn =TRUEWHERE datname ='template0';UPDATE1priv_test=#\c template0
You are now connected todatabase"template0"asuser"ads".template0=#\dn
List of schemas
Name|Owner--------+-------------------
public | pg_database_owner
(1row)template0=#\dn+ public
List of schemas
Name|Owner|Accessprivileges| Description
--------+-------------------+----------------------------------------+------------------------
public | pg_database_owner | pg_database_owner=UC/pg_database_owner+| standard public schema||=U/pg_database_owner |(1row)
As we can see, public in template0 is missing the CREATE privilege, and this is then copied into the new database. Any application expecting otherwise and depending on public being writable by regular users will run into problems. This problem can be fixed by adding the CREATE privilege for public in the new database:
1
2
3
4
5
6
7
8
9
10
priv_test=#GRANTCREATEONSCHEMA public TO PUBLIC;GRANTpriv_test=#\dn+ public
List of schemas
Name|Owner|Accessprivileges| Description
--------+-------------------+----------------------------------------+------------------------
public | pg_database_owner | pg_database_owner=UC/pg_database_owner+| standard public schema||=UC/pg_database_owner |(1row)
If you want this the default for new databases, apply this change to template1.
pg_upgrade
The second way for upgrades is pg_upgrade. This copies the catalog from the old database into the new one, and then either copies or links the data files. This is a smooth way to upgrade a server to a newer version.
This runs pg_upgrade and updates version 14 to version 15. Afterwards the public schema looks the same as in version 14:
1
2
3
4
5
6
7
priv_test=#\dn+ public
List of schemas
Name|Owner|Accessprivileges| Description
--------+-------+-------------------+------------------------
public | ads | ads=UC/ads +| standard public schema||=UC/ads |(1row)
PostgreSQL does not apply the pg_database_owner, and does not set (revoke) the “new” permissions. Instead everything is copied as it was in the previous version. If you want to have the new behavior from version 15, you need to revoke CREATE from public:
1
2
priv_test=#REVOKECREATEONSCHEMA public FROM PUBLIC;REVOKE
You can also set the owner to the new pg_database_owner:
1
2
3
4
5
6
7
8
9
10
priv_test=#ALTERSCHEMA public OWNERTO pg_database_owner;ALTERSCHEMApriv_test=#\dn+ public
List of schemas
Name|Owner|Accessprivileges| Description
--------+-------------------+----------------------------------------+------------------------
public | pg_database_owner | pg_database_owner=UC/pg_database_owner+| standard public schema||=U/pg_database_owner |(1row)
Summary
Upgrading a PostgreSQL database from version <= 14 to version 15 or newer brings a few challenges regarding the public schema. Both of the most common upgrade ways behave differently while dealing with the changes.
It’s best not to rely on a writable public schema at all.