Skip to content

Lock down the public schema

Severity: mediumApplies to: PostgreSQL 15+ (fresh clusters)Applies to: Any cluster upgraded from 14 or earlier
The fix
-- Run against every database, not just one
ALTER SCHEMA public OWNER TO pg_database_owner;
REVOKE CREATE ON SCHEMA public FROM PUBLIC;

On a cluster created fresh with PostgreSQL 15 or later, this is already done. On a cluster upgraded from 14 or earlier, it is not — see below.

Before PostgreSQL 15, the public schema granted CREATE to PUBLIC — that is, to every role in the database. Any user who could connect could create objects there.

That sounds like a tidiness problem. It’s a privilege-escalation primitive, because of search_path. The default search_path puts public first, so an unprivileged user can create a function or operator in public that shadows one a privileged user is about to call. When the superuser (or a SECURITY DEFINER function) runs a query that resolves to the attacker’s object, it executes with the caller’s privileges.

PostgreSQL 15 changed this: public is now owned by the new pg_database_owner role, and CREATE is no longer granted to PUBLIC. USAGE is still granted, so reading and using existing objects still works — only creating them is restricted.

This is the part that matters, and it is easy to miss entirely:

Upgrading does not give you the new behaviour. pg_upgrade and pg_dump faithfully preserve the original privileges, because silently revoking grants during an upgrade would break applications. The secure default applies to databases created by initdb on 15+, and to new databases created from template1/template0 on 15+ — not to databases carried forward.

So a cluster reporting PostgreSQL 18 may still have the pre-15 permissive public schema, and nothing in SELECT version() reveals it. The version number is not the answer to this question; the ACL is.

If you inherited a long-lived cluster, assume it is unfixed until you have checked the ACL.

public is a schema, and schemas live inside databases. Fixing it in postgres does nothing for appdb. Iterate:

Terminal window
for db in $(psql -At -c "SELECT datname FROM pg_database WHERE datallowconn AND NOT datistemplate"); do
psql -d "$db" -c 'ALTER SCHEMA public OWNER TO pg_database_owner;
REVOKE CREATE ON SCHEMA public FROM PUBLIC;'
done

Fix template1 too, or every database you create from now on inherits the old grants again.