Lock down the public schema
-- Run against every database, not just oneALTER 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.
Why it matters
Section titled “Why it matters”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.
The upgrade trap
Section titled “The upgrade trap”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.
Do it per database
Section titled “Do it per database”public is a schema, and schemas live inside databases. Fixing it in postgres
does nothing for appdb. Iterate:
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;'doneFix template1 too, or every database you create from now on inherits the old
grants again.
Related
Section titled “Related”- Grant least privilege to roles — the rest of the privilege story.