On Sat, Mar 27, 2021 at 11:41:07AM +0100, Laurenz Albe wrote: > On Sat, 2021-03-27 at 00:50 -0700, Noah Misch wrote: > > On Sat, Feb 13, 2021 at 04:56:29AM -0800, Noah Misch wrote: > > > I'm attaching the patch for $SUBJECT, which applies atop the four patches > > > from > > > the two other threads below. For convenience of testing, I've included a > > > rollup patch, equivalent to applying all five patches. > > > > I committed prerequisites from one thread, so here's a rebased rollup patch. > > I am happy to see this problem tackled!
Rebased. I've pushed all prerequisites, so there's no longer a distinct rollup patch.
Author: Noah Misch <n...@leadboat.com> Commit: Noah Misch <n...@leadboat.com> Revoke PUBLIC CREATE from public schema, now owned by pg_database_owner. This switches the default ACL to what the documentation has recommended since CVE-2018-1058. Upgrades will carry forward any old ownership and ACL. Sites that declined the 2018 recommendation should take a fresh look. Recipes for commissioning a new database cluster from scratch may need to create a schema, grant more privileges, etc. Out-of-tree test suites may require such updates. Reviewed by FIXME. Discussion: https://postgr.es/m/20201031163518.gb4039...@rfd.leadboat.com diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out index 31b5de9..3d77cea 100644 --- a/contrib/postgres_fdw/expected/postgres_fdw.out +++ b/contrib/postgres_fdw/expected/postgres_fdw.out @@ -9215,7 +9215,7 @@ $d$; -- But creation of user mappings for non-superusers should fail CREATE USER MAPPING FOR public SERVER loopback_nopw; CREATE USER MAPPING FOR CURRENT_USER SERVER loopback_nopw; -CREATE FOREIGN TABLE ft1_nopw ( +CREATE FOREIGN TABLE pg_temp.ft1_nopw ( c1 int NOT NULL, c2 int NOT NULL, c3 text, diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql index 286dd99..36db65f 100644 --- a/contrib/postgres_fdw/sql/postgres_fdw.sql +++ b/contrib/postgres_fdw/sql/postgres_fdw.sql @@ -2735,7 +2735,7 @@ $d$; CREATE USER MAPPING FOR public SERVER loopback_nopw; CREATE USER MAPPING FOR CURRENT_USER SERVER loopback_nopw; -CREATE FOREIGN TABLE ft1_nopw ( +CREATE FOREIGN TABLE pg_temp.ft1_nopw ( c1 int NOT NULL, c2 int NOT NULL, c3 text, diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 4986548..e84c41a 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -3001,20 +3001,18 @@ SELECT 3 OPERATOR(pg_catalog.+) 4; <para> By default, users cannot access any objects in schemas they do not own. To allow that, the owner of the schema must grant the - <literal>USAGE</literal> privilege on the schema. To allow users - to make use of the objects in the schema, additional privileges - might need to be granted, as appropriate for the object. + <literal>USAGE</literal> privilege on the schema. By default, everyone + has that privilege on the schema <literal>public</literal>. To allow + users to make use of the objects in a schema, additional privileges might + need to be granted, as appropriate for the object. </para> <para> - A user can also be allowed to create objects in someone else's - schema. To allow that, the <literal>CREATE</literal> privilege on - the schema needs to be granted. Note that by default, everyone - has <literal>CREATE</literal> and <literal>USAGE</literal> privileges on - the schema - <literal>public</literal>. This allows all users that are able to - connect to a given database to create objects in its - <literal>public</literal> schema. + A user can also be allowed to create objects in someone else's schema. To + allow that, the <literal>CREATE</literal> privilege on the schema needs to + be granted. In databases upgraded from + <productname>PostgreSQL</productname> 13 or earlier, everyone has that + privilege on the schema <literal>public</literal>. Some <link linkend="ddl-schemas-patterns">usage patterns</link> call for revoking that privilege: <programlisting> @@ -3087,20 +3085,25 @@ REVOKE CREATE ON SCHEMA public FROM PUBLIC; database owner attack. --> <para> Constrain ordinary users to user-private schemas. To implement this, - issue <literal>REVOKE CREATE ON SCHEMA public FROM PUBLIC</literal>, - and create a schema for each user with the same name as that user. - Recall that the default search path starts - with <literal>$user</literal>, which resolves to the user name. - Therefore, if each user has a separate schema, they access their own - schemas by default. After adopting this pattern in a database where - untrusted users had already logged in, consider auditing the public - schema for objects named like objects in + first issue <literal>REVOKE CREATE ON SCHEMA public FROM + PUBLIC</literal>. Then, for every user needing to create non-temporary + objects, create a schema with the same name as that user. Recall that + the default search path starts with <literal>$user</literal>, which + resolves to the user name. Therefore, if each user has a separate + schema, they access their own schemas by default. After adopting this + pattern in a database where untrusted users had already logged in, + consider auditing the public schema for objects named like objects in schema <literal>pg_catalog</literal>. This pattern is a secure schema usage pattern unless an untrusted user is the database owner or holds the <literal>CREATEROLE</literal> privilege, in which case no secure schema usage pattern exists. </para> <para> + If the database originated in an upgrade + from <productname>PostgreSQL</productname> 13 or earlier, + the <literal>REVOKE</literal> is essential. Otherwise, the default + configuration follows this pattern; ordinary users can create only + temporary objects until a privileged user furnishes a schema. </para> </listitem> @@ -3109,10 +3112,10 @@ REVOKE CREATE ON SCHEMA public FROM PUBLIC; Remove the public schema from the default search path, by modifying <link linkend="config-setting-configuration-file"><filename>postgresql.conf</filename></link> or by issuing <literal>ALTER ROLE ALL SET search_path = - "$user"</literal>. Everyone retains the ability to create objects in - the public schema, but only qualified names will choose those objects. - While qualified table references are fine, calls to functions in the - public schema <link linkend="typeconv-func">will be unsafe or + "$user"</literal>. Then, grant privileges to create in the public + schema. Only qualified names will choose public schema objects. While + qualified table references are fine, calls to functions in the public + schema <link linkend="typeconv-func">will be unsafe or unreliable</link>. If you create functions or extensions in the public schema, use the first pattern instead. Otherwise, like the first pattern, this is secure unless an untrusted user is the database owner @@ -3122,11 +3125,14 @@ REVOKE CREATE ON SCHEMA public FROM PUBLIC; <listitem> <para> - Keep the default. All users access the public schema implicitly. This + Keep the default search path, and grant privileges to create in the + public schema. All users access the public schema implicitly. This simulates the situation where schemas are not available at all, giving a smooth transition from the non-schema-aware world. However, this is never a secure pattern. It is acceptable only when the database has a - single user or a few mutually-trusting users. + single user or a few mutually-trusting users. In databases upgraded + from <productname>PostgreSQL</productname> 13 or earlier, this is the + default. </para> </listitem> </itemizedlist> diff --git a/doc/src/sgml/user-manag.sgml b/doc/src/sgml/user-manag.sgml index fe0bdb7..7b7c488 100644 --- a/doc/src/sgml/user-manag.sgml +++ b/doc/src/sgml/user-manag.sgml @@ -596,13 +596,14 @@ DROP ROLE doomed_role; <para> The <literal>pg_database_owner</literal> role has one implicit, - situation-dependent member, namely the owner of the current database. The - role conveys no rights at first. Like any role, it can own objects or - receive grants of access privileges. Consequently, once - <literal>pg_database_owner</literal> has rights within a template database, - each owner of a database instantiated from that template will exercise those - rights. <literal>pg_database_owner</literal> cannot be a member of any - role, and it cannot have non-implicit members. + situation-dependent member, namely the owner of the current database. Like + any role, it can own objects or receive grants of access privileges. + Consequently, once <literal>pg_database_owner</literal> has rights within a + template database, each owner of a database instantiated from that template + will exercise those rights. <literal>pg_database_owner</literal> cannot be + a member of any role, and it cannot have non-implicit members. Initially, + this role owns the <literal>public</literal> schema, so each database owner + governs local use of the schema. </para> <para> @@ -651,8 +652,8 @@ GRANT pg_signal_backend TO admin_user; horse</quote> others with relative ease. The strongest protection is tight control over who can define objects. Where that is infeasible, write queries referring only to objects having trusted owners. Remove - from <varname>search_path</varname> the public schema and any other schemas - that permit untrusted users to create objects. + from <varname>search_path</varname> any schemas that permit untrusted users + to create objects. </para> <para> diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c index 152d21e..eba3385 100644 --- a/src/bin/initdb/initdb.c +++ b/src/bin/initdb/initdb.c @@ -1703,8 +1703,7 @@ setup_privileges(FILE *cmdfd) CppAsString2(RELKIND_VIEW) ", " CppAsString2(RELKIND_MATVIEW) ", " CppAsString2(RELKIND_SEQUENCE) ")" " AND relacl IS NULL;\n\n", - "GRANT USAGE ON SCHEMA pg_catalog TO PUBLIC;\n\n", - "GRANT CREATE, USAGE ON SCHEMA public TO PUBLIC;\n\n", + "GRANT USAGE ON SCHEMA pg_catalog, public TO PUBLIC;\n\n", "REVOKE ALL ON pg_largeobject FROM PUBLIC;\n\n", "INSERT INTO pg_init_privs " " (objoid, classoid, objsubid, initprivs, privtype)" diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index 3211521..37e6be7 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -1647,11 +1647,12 @@ selectDumpableNamespace(NamespaceInfo *nsinfo, Archive *fout) * no-mans-land between being a system object and a user object. * CREATE SCHEMA would fail, so its DUMP_COMPONENT_DEFINITION is just * a comment and an indication of ownership. If the owner is the - * default, that DUMP_COMPONENT_DEFINITION is superfluous. + * default, omit that superfluous DUMP_COMPONENT_DEFINITION. Before + * v14, the default owner was BOOTSTRAP_SUPERUSERID. */ nsinfo->create = false; nsinfo->dobj.dump = DUMP_COMPONENT_ALL; - if (nsinfo->nspowner == BOOTSTRAP_SUPERUSERID) + if (nsinfo->nspowner == ROLE_PG_DATABASE_OWNER) nsinfo->dobj.dump &= ~DUMP_COMPONENT_DEFINITION; nsinfo->dobj.dump_contains = DUMP_COMPONENT_ALL; } @@ -4867,21 +4868,26 @@ getNamespaces(Archive *fout, int *numNamespaces) PQExpBuffer init_racl_subquery = createPQExpBuffer(); /* - * Bypass pg_init_privs.initprivs for the public schema. Dropping and - * recreating the schema detaches it from its pg_init_privs row, but - * an empty destination database starts with this ACL nonetheless. - * Also, we support dump/reload of public schema ownership changes. - * ALTER SCHEMA OWNER filters nspacl through aclnewowner(), but - * initprivs continues to reflect the initial owner (the bootstrap - * superuser). Hence, synthesize the value that nspacl will have - * after the restore's ALTER SCHEMA OWNER. + * Bypass pg_init_privs.initprivs for the public schema, for several + * reasons. First, dropping and recreating the schema detaches it + * from its pg_init_privs row, but an empty destination database + * starts with this ACL nonetheless. Second, we support dump/reload + * of public schema ownership changes. ALTER SCHEMA OWNER filters + * nspacl through aclnewowner(), but initprivs continues to reflect + * the initial owner. Hence, synthesize the value that nspacl will + * have after the restore's ALTER SCHEMA OWNER. Third, this makes the + * destination database match the source's ACL, even if the latter was + * an initdb-default ACL, which changed in v14. An upgrade pulls in + * changes to most system object ACLs that the DBA had not customized. + * We've made the public schema depart from that, because changing its + * ACL so easily breaks applications. */ buildACLQueries(acl_subquery, racl_subquery, init_acl_subquery, init_racl_subquery, "n.nspacl", "n.nspowner", "CASE WHEN n.nspname = 'public' THEN array[" " format('%s=UC/%s', " " n.nspowner::regrole, n.nspowner::regrole)," - " format('=UC/%s', n.nspowner::regrole)]::aclitem[] " + " format('=U/%s', n.nspowner::regrole)]::aclitem[] " "ELSE pip.initprivs END", "'n'", dopt->binary_upgrade); diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index 448b1be2..edfbe41 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -629,7 +629,9 @@ my %tests = ( }, 'ALTER SCHEMA public OWNER TO' => { - # see test "REVOKE CREATE ON SCHEMA public" for causative create_sql + create_order => 15, + create_sql => + 'ALTER SCHEMA public OWNER TO "regress_quoted \"" role";', regexp => qr/^ALTER SCHEMA public OWNER TO .+;/m, like => { %full_runs, section_pre_data => 1, @@ -3414,17 +3416,12 @@ my %tests = ( unlike => { no_privs => 1, }, }, - 'REVOKE CREATE ON SCHEMA public FROM public' => { + 'REVOKE ALL ON SCHEMA public' => { create_order => 16, - create_sql => ' - REVOKE CREATE ON SCHEMA public FROM public; - ALTER SCHEMA public OWNER TO "regress_quoted \"" role"; - REVOKE ALL ON SCHEMA public FROM "regress_quoted \"" role";', - regexp => qr/^ - \QREVOKE ALL ON SCHEMA public FROM "regress_quoted \E\\""\ role"; - \n\QREVOKE ALL ON SCHEMA public FROM PUBLIC;\E - \n\QGRANT USAGE ON SCHEMA public TO PUBLIC;\E - /xm, + create_sql => + 'REVOKE ALL ON SCHEMA public FROM "regress_quoted \"" role";', + regexp => + qr/^REVOKE ALL ON SCHEMA public FROM "regress_quoted \\"" role";/m, like => { %full_runs, section_pre_data => 1, }, unlike => { no_privs => 1, }, }, diff --git a/src/include/catalog/pg_namespace.dat b/src/include/catalog/pg_namespace.dat index 33992af..9a23975 100644 --- a/src/include/catalog/pg_namespace.dat +++ b/src/include/catalog/pg_namespace.dat @@ -21,6 +21,6 @@ # update dumpNamespace() if changing this descr { oid => '2200', oid_symbol => 'PG_PUBLIC_NAMESPACE', descr => 'standard public schema', - nspname => 'public', nspacl => '_null_' }, + nspname => 'public', nspowner => 'pg_database_owner', nspacl => '_null_' }, ] diff --git a/src/pl/plperl/expected/plperl_setup.out b/src/pl/plperl/expected/plperl_setup.out index a1a24df..5234feb 100644 --- a/src/pl/plperl/expected/plperl_setup.out +++ b/src/pl/plperl/expected/plperl_setup.out @@ -25,6 +25,9 @@ CREATE EXTENSION plperl; CREATE EXTENSION plperlu; -- fail ERROR: permission denied to create extension "plperlu" HINT: Must be superuser to create this extension. +CREATE SCHEMA plperl_setup_scratch; +SET search_path = plperl_setup_scratch; +GRANT ALL ON SCHEMA plperl_setup_scratch TO regress_user2; CREATE FUNCTION foo1() returns int language plperl as '1;'; SELECT foo1(); foo1 @@ -34,6 +37,7 @@ SELECT foo1(); -- Must reconnect to avoid failure with non-MULTIPLICITY Perl interpreters \c - +SET search_path = plperl_setup_scratch; SET ROLE regress_user1; -- Should be able to change privileges on the language revoke all on language plperl from public; diff --git a/src/pl/plperl/sql/plperl_setup.sql b/src/pl/plperl/sql/plperl_setup.sql index 7484478..a89cf56 100644 --- a/src/pl/plperl/sql/plperl_setup.sql +++ b/src/pl/plperl/sql/plperl_setup.sql @@ -27,12 +27,16 @@ SET ROLE regress_user1; CREATE EXTENSION plperl; CREATE EXTENSION plperlu; -- fail +CREATE SCHEMA plperl_setup_scratch; +SET search_path = plperl_setup_scratch; +GRANT ALL ON SCHEMA plperl_setup_scratch TO regress_user2; CREATE FUNCTION foo1() returns int language plperl as '1;'; SELECT foo1(); -- Must reconnect to avoid failure with non-MULTIPLICITY Perl interpreters \c - +SET search_path = plperl_setup_scratch; SET ROLE regress_user1; diff --git a/src/test/regress/input/tablespace.source b/src/test/regress/input/tablespace.source index c133e73..cb9774e 100644 --- a/src/test/regress/input/tablespace.source +++ b/src/test/regress/input/tablespace.source @@ -388,7 +388,7 @@ CREATE INDEX k ON testschema.tablespace_acl (c) TABLESPACE regress_tblspace; ALTER TABLE testschema.tablespace_acl OWNER TO regress_tablespace_user2; SET SESSION ROLE regress_tablespace_user2; -CREATE TABLE tablespace_table (i int) TABLESPACE regress_tblspace; -- fail +CREATE TEMP TABLE tablespace_table (i int) TABLESPACE regress_tblspace; -- fail ALTER TABLE testschema.tablespace_acl ALTER c TYPE bigint; REINDEX (TABLESPACE regress_tblspace) TABLE tablespace_table; -- fail REINDEX (TABLESPACE regress_tblspace, CONCURRENTLY) TABLE tablespace_table; -- fail @@ -409,3 +409,6 @@ DROP SCHEMA testschema CASCADE; DROP ROLE regress_tablespace_user1; DROP ROLE regress_tablespace_user2; + +-- Rest of this suite can use the public schema freely. +GRANT ALL ON SCHEMA public TO public; diff --git a/src/test/regress/output/tablespace.source b/src/test/regress/output/tablespace.source index 1bbe7e0..e7629d4 100644 --- a/src/test/regress/output/tablespace.source +++ b/src/test/regress/output/tablespace.source @@ -908,7 +908,7 @@ CREATE TABLE testschema.tablespace_acl (c int); CREATE INDEX k ON testschema.tablespace_acl (c) TABLESPACE regress_tblspace; ALTER TABLE testschema.tablespace_acl OWNER TO regress_tablespace_user2; SET SESSION ROLE regress_tablespace_user2; -CREATE TABLE tablespace_table (i int) TABLESPACE regress_tblspace; -- fail +CREATE TEMP TABLE tablespace_table (i int) TABLESPACE regress_tblspace; -- fail ERROR: permission denied for tablespace regress_tblspace ALTER TABLE testschema.tablespace_acl ALTER c TYPE bigint; REINDEX (TABLESPACE regress_tblspace) TABLE tablespace_table; -- fail @@ -934,3 +934,5 @@ drop cascades to table testschema.atable drop cascades to table testschema.tablespace_acl DROP ROLE regress_tablespace_user1; DROP ROLE regress_tablespace_user2; +-- Rest of this suite can use the public schema freely. +GRANT ALL ON SCHEMA public TO public;