]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
Disallow CR and LF in database, role, and tablespace names
authorAndrew Dunstan <andrew@dunslane.net>
Sat, 21 Feb 2026 22:17:48 +0000 (17:17 -0500)
committerAndrew Dunstan <andrew@dunslane.net>
Mon, 23 Feb 2026 16:19:13 +0000 (11:19 -0500)
Previously, these characters could cause problems when passed through
shell commands, and were flagged with a comment in string_utils.c
suggesting they be rejected in a future major release.

The affected commands are CREATE DATABASE, CREATE ROLE, CREATE TABLESPACE,
ALTER DATABASE RENAME, ALTER ROLE RENAME, and ALTER TABLESPACE RENAME.

Also add a pg_upgrade check to detect these invalid names in clusters
being upgraded from pre-v19 versions, producing a report file listing
any offending objects that must be renamed before upgrading.

Tests have been modified accordingly.

Author: Mahendra Singh Thalor <mahi6run@gmail.com>
Reviewed-By: Álvaro Herrera <alvherre@alvh.no-ip.org>
Reviewed-By: Andrew Dunstan <andrew@dunslane.net>
Reviewed-By: Tom Lane <tgl@sss.pgh.pa.us>
Reviewed-By: Nathan Bossart <nathandbossart@gmail.com>
Reviewed-By: Srinath Reddy <srinath2133@gmail.com>
Discussion: https://postgr.es/m/CAKYtNApkOi4FY0S7+3jpTqnHVyyZ6Tbzhtbah-NBbY-mGsiKAQ@mail.gmail.com

15 files changed:
src/backend/commands/dbcommands.c
src/backend/commands/tablespace.c
src/backend/commands/user.c
src/bin/pg_dump/t/002_pg_dump.pl
src/bin/pg_dump/t/003_pg_dump_with_server.pl
src/bin/pg_dump/t/010_dump_connstr.pl
src/bin/pg_upgrade/check.c
src/bin/scripts/t/020_createdb.pl
src/fe_utils/string_utils.c
src/test/modules/unsafe_tests/expected/alter_system_table.out
src/test/modules/unsafe_tests/expected/rolenames.out
src/test/modules/unsafe_tests/sql/alter_system_table.sql
src/test/modules/unsafe_tests/sql/rolenames.sql
src/test/regress/expected/tablespace.out
src/test/regress/sql/tablespace.sql

index 33311760df7e19f638cf78ed4e231b301299cd58..740c526e92d9464355c0369320f519f1d956f09d 100644 (file)
@@ -743,6 +743,12 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
        CreateDBStrategy dbstrategy = CREATEDB_WAL_LOG;
        createdb_failure_params fparms;
 
+       /* Report error if name has \n or \r character. */
+       if (strpbrk(dbname, "\n\r"))
+               ereport(ERROR,
+                               (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+                                errmsg("database name \"%s\" contains a newline or carriage return character", dbname)));
+
        /* Extract options from the statement node tree */
        foreach(option, stmt->options)
        {
@@ -1911,6 +1917,12 @@ RenameDatabase(const char *oldname, const char *newname)
        int                     npreparedxacts;
        ObjectAddress address;
 
+       /* Report error if name has \n or \r character. */
+       if (strpbrk(newname, "\n\r"))
+               ereport(ERROR,
+                               (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+                                errmsg("database name \"%s\" contains a newline or carriage return character", newname)));
+
        /*
         * Look up the target database's OID, and get exclusive lock on it. We
         * need this for the same reasons as DROP DATABASE.
index 3511a4ec0fdea005849a8a3a05982eee8e8134a4..ed2a93a09dbefdeec3683954dd9c1261157b6f28 100644 (file)
@@ -242,6 +242,12 @@ CreateTableSpace(CreateTableSpaceStmt *stmt)
                                (errcode(ERRCODE_INVALID_NAME),
                                 errmsg("tablespace location cannot contain single quotes")));
 
+       /* Report error if name has \n or \r character. */
+       if (strpbrk(stmt->tablespacename, "\n\r"))
+               ereport(ERROR,
+                               (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+                                errmsg("tablespace name \"%s\" contains a newline or carriage return character", stmt->tablespacename)));
+
        in_place = allow_in_place_tablespaces && strlen(location) == 0;
 
        /*
@@ -971,6 +977,12 @@ RenameTableSpace(const char *oldname, const char *newname)
                                 errmsg("unacceptable tablespace name \"%s\"", newname),
                                 errdetail("The prefix \"pg_\" is reserved for system tablespaces.")));
 
+       /* Report error if name has \n or \r character. */
+       if (strpbrk(newname, "\n\r"))
+               ereport(ERROR,
+                               (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+                                errmsg("tablespace name \"%s\" contains a newline or carriage return character", newname)));
+
        /*
         * If built with appropriate switch, whine when regression-testing
         * conventions for tablespace names are violated.
index 8fb9ea25db0d80c4e3f9aef9eeabe38a0d1c548c..be11c49f919d0e8cee6c2cdb0dc933d2a644f868 100644 (file)
@@ -171,6 +171,12 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
        DefElem    *dbypassRLS = NULL;
        GrantRoleOptions popt;
 
+       /* Report error if name has \n or \r character. */
+       if (strpbrk(stmt->role, "\n\r"))
+               ereport(ERROR,
+                               (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+                                errmsg("role name \"%s\" contains a newline or carriage return character", stmt->role)));
+
        /* The defaults can vary depending on the original statement type */
        switch (stmt->stmt_type)
        {
@@ -1348,6 +1354,12 @@ RenameRole(const char *oldname, const char *newname)
        ObjectAddress address;
        Form_pg_authid authform;
 
+       /* Report error if name has \n or \r character. */
+       if (strpbrk(newname, "\n\r"))
+               ereport(ERROR,
+                               (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+                                errmsg("role name \"%s\" contains a newline or carriage return character", newname)));
+
        rel = table_open(AuthIdRelationId, RowExclusiveLock);
        dsc = RelationGetDescr(rel);
 
index a8dcc2b5c757ad29de0d3e5a5d20219d473ec869..bc7a082f57ace243ee6a91e17e2e79bdcffea074 100644 (file)
@@ -2048,13 +2048,8 @@ my %tests = (
                },
        },
 
-       'newline of role or table name in comment' => {
-               create_sql => qq{CREATE ROLE regress_newline;
-                                                ALTER ROLE regress_newline SET enable_seqscan = off;
-                                                ALTER ROLE regress_newline
-                                                       RENAME TO "regress_newline\nattack";
-
-                                                -- meet getPartitioningInfo() "unsafe" condition
+       'newline of table name in comment' => {
+               create_sql => qq{-- meet getPartitioningInfo() "unsafe" condition
                                                 CREATE TYPE pp_colors AS
                                                        ENUM ('green', 'blue', 'black');
                                                 CREATE TABLE pp_enumpart (a pp_colors)
index 70f830e10dca212f6cc4b161b3ac4f58aac8a6a3..349add67d5043c3d5d801975af2c5297ac6bc0ab 100644 (file)
@@ -16,22 +16,6 @@ my $port = $node->port;
 $node->init;
 $node->start;
 
-#########################################
-# pg_dumpall: newline in database name
-
-$node->safe_psql('postgres', qq{CREATE DATABASE "regress_\nattack"});
-
-my (@cmd, $stdout, $stderr);
-@cmd = ("pg_dumpall", '--port' => $port, '--exclude-database=postgres');
-print("# Running: " . join(" ", @cmd) . "\n");
-my $result = IPC::Run::run \@cmd, '>' => \$stdout, '2>' => \$stderr;
-ok(!$result, "newline in dbname: exit code not 0");
-like(
-       $stderr,
-       qr/shell command argument contains a newline/,
-       "newline in dbname: stderr matches");
-unlike($stdout, qr/^attack/m, "newline in dbname: no comment escape");
-
 #########################################
 # Verify that dumping foreign data includes only foreign tables of
 # matching servers
index dc7a33658db07ab98e00a71690fdac200d054d97..bf2c3b6d00bd4dc11c65284b04fb443a67282d0d 100644 (file)
@@ -153,20 +153,6 @@ $node->command_ok(
        ],
        'pg_dumpall --dbname accepts connection string');
 
-$node->run_log(
-       [ 'createdb', '--username' => $src_bootstrap_super, "foo\n\rbar" ]);
-
-# not sufficient to use --roles-only here
-$node->command_fails(
-       [
-               'pg_dumpall', '--no-sync',
-               '--username' => $src_bootstrap_super,
-               '--file' => $discard,
-       ],
-       'pg_dumpall with \n\r in database name');
-$node->run_log(
-       [ 'dropdb', '--username' => $src_bootstrap_super, "foo\n\rbar" ]);
-
 
 # make a table, so the parallel worker has something to dump
 $node->safe_psql(
index 5c73773bf0eeed6254c0eae95cc4e219cc178201..c0e7ec351688d40051ac9b734be56ffe21ca44d8 100644 (file)
@@ -34,6 +34,7 @@ static void check_new_cluster_replication_slots(void);
 static void check_new_cluster_subscription_configuration(void);
 static void check_old_cluster_for_valid_slots(void);
 static void check_old_cluster_subscription_state(void);
+static void check_old_cluster_global_names(ClusterInfo *cluster);
 
 /*
  * DataTypesUsageChecks - definitions of data type checks for the old cluster
@@ -600,6 +601,14 @@ check_and_dump_old_cluster(void)
         */
        check_for_connection_status(&old_cluster);
 
+       /*
+        * Validate database, user, role and tablespace names from the old cluster.
+        * No need to check in 19 or newer as newline and carriage return are
+        * not allowed at the creation time of the object.
+        */
+       if (GET_MAJOR_VERSION(old_cluster.major_version) < 1900)
+               check_old_cluster_global_names(&old_cluster);
+
        /*
         * Extract a list of databases, tables, and logical replication slots from
         * the old cluster.
@@ -2500,3 +2509,73 @@ check_old_cluster_subscription_state(void)
        else
                check_ok();
 }
+
+/*
+ * check_old_cluster_global_names()
+ *
+ * Raise an error if any database, role, or tablespace name contains a newline
+ * or carriage return character.  Such names are not allowed in v19 and later.
+ */
+static void
+check_old_cluster_global_names(ClusterInfo *cluster)
+{
+       int                     i;
+       PGconn          *conn_template1;
+       PGresult        *res;
+       int                     ntups;
+       FILE            *script = NULL;
+       char            output_path[MAXPGPATH];
+       int                     count = 0;
+
+       prep_status("Checking names of databases, roles and tablespaces");
+
+       snprintf(output_path, sizeof(output_path), "%s/%s",
+                       log_opts.basedir,
+                       "db_role_tablespace_invalid_names.txt");
+
+       conn_template1 = connectToServer(cluster, "template1");
+
+       /*
+        * Get database, user/role and tablespacenames from cluster.  Can't use
+        * pg_authid because only superusers can view it.
+        */
+       res = executeQueryOrDie(conn_template1,
+                       "SELECT datname AS objname, 'database' AS objtype "
+                       "FROM pg_catalog.pg_database UNION ALL "
+                       "SELECT rolname AS objname, 'role' AS objtype "
+                       "FROM pg_catalog.pg_roles UNION ALL "
+                       "SELECT spcname AS objname, 'tablespace' AS objtype "
+                       "FROM pg_catalog.pg_tablespace ORDER BY 2 ");
+
+       ntups = PQntuples(res);
+       for (i = 0; i < ntups; i++)
+       {
+               char    *objname = PQgetvalue(res, i, 0);
+               char    *objtype = PQgetvalue(res, i, 1);
+
+               /* If name has \n or \r, then report it. */
+               if (strpbrk(objname, "\n\r"))
+               {
+                       if (script == NULL && (script = fopen_priv(output_path, "w")) == NULL)
+                               pg_fatal("could not open file \"%s\": %m", output_path);
+
+                       fprintf(script, "%d : %s name = \"%s\"\n", ++count, objtype, objname);
+               }
+       }
+
+       PQclear(res);
+       PQfinish(conn_template1);
+
+       if (script)
+       {
+               fclose(script);
+               pg_log(PG_REPORT, "fatal");
+               pg_fatal("All the database, role and tablespace names should have only valid characters. A newline or \n"
+                               "carriage return character is not allowed in these object names.  To fix this, please \n"
+                               "rename these names with valid names. \n"
+                               "To see all %d invalid object names, refer db_role_tablespace_invalid_names.txt file. \n"
+                               "    %s", count, output_path);
+       }
+       else
+               check_ok();
+}
index 83b0077383abe2de323db3a19344bfd4dd3961db..a0995868363c438fe4158a209e2dc67f83275fc4 100644 (file)
@@ -241,6 +241,18 @@ $node->command_fails(
        ],
        'fails for invalid locale provider');
 
+$node->command_fails_like(
+    [ 'createdb', "invalid \n dbname" ],
+    qr(contains a newline or carriage return character),
+    'fails if database name contains a newline character in name'
+);
+
+$node->command_fails_like(
+    [ 'createdb', "invalid \r dbname" ],
+    qr(contains a newline or carriage return character),
+    'fails if database name contains a carriage return character in name'
+);
+
 # Check use of templates with shared dependencies copied from the template.
 my ($ret, $stdout, $stderr) = $node->psql(
        'foobar2',
index 89ce396ed4f3e0382814c1f568a1798c2fecaec7..38fffbd036bf79cfd80b70c56aaa0c0166730477 100644 (file)
@@ -568,12 +568,6 @@ appendByteaLiteral(PQExpBuffer buf, const unsigned char *str, size_t length,
  * Append the given string to the shell command being built in the buffer,
  * with shell-style quoting as needed to create exactly one argument.
  *
- * Forbid LF or CR characters, which have scant practical use beyond designing
- * security breaches.  The Windows command shell is unusable as a conduit for
- * arguments containing LF or CR characters.  A future major release should
- * reject those characters in CREATE ROLE and CREATE DATABASE, because use
- * there eventually leads to errors here.
- *
  * appendShellString() simply prints an error and dies if LF or CR appears.
  * appendShellStringNoError() omits those characters from the result, and
  * returns false if there were any.
index b73b9442b8d4d5549ade2a0b572ec69e0e0160c1..9ea6061f4ae76ad532fc7eaddd1404c3edaa0d3c 100644 (file)
@@ -64,6 +64,11 @@ ERROR:  permission denied: "pg_description" is a system catalog
 CREATE TABLESPACE pg_foo LOCATION '/no/such/location';
 ERROR:  unacceptable tablespace name "pg_foo"
 DETAIL:  The prefix "pg_" is reserved for system tablespaces.
+-- contains \n\r tablespace name
+CREATE TABLESPACE "invalid
+name" LOCATION '/no/such/location';
+ERROR:  tablespace name "invalid
+name" contains a newline or carriage return character
 -- triggers
 CREATE FUNCTION tf1() RETURNS trigger
 LANGUAGE plpgsql
index 61396b2a8054ae0b0ff8477ec287528570aaf1e0..9dfb8475e164b60cf8abdd3eeed3759c5008893b 100644 (file)
@@ -102,6 +102,10 @@ DETAIL:  Role names starting with "pg_" are reserved.
 CREATE ROLE "pg_abcdef"; -- error
 ERROR:  role name "pg_abcdef" is reserved
 DETAIL:  Role names starting with "pg_" are reserved.
+CREATE ROLE "invalid
+rolename"; -- error
+ERROR:  role name "invalid
+rolename" contains a newline or carriage return character
 CREATE ROLE regress_testrol0 SUPERUSER LOGIN;
 CREATE ROLE regress_testrolx SUPERUSER LOGIN;
 CREATE ROLE regress_testrol2 SUPERUSER;
index c1515100845cd64d2e2b12eb813601a30225516f..1b0eb727eaaab930aa15553814f10120321ec609 100644 (file)
@@ -64,6 +64,10 @@ ALTER TABLE pg_description SET SCHEMA public;
 -- reserved tablespace name
 CREATE TABLESPACE pg_foo LOCATION '/no/such/location';
 
+-- contains \n\r tablespace name
+CREATE TABLESPACE "invalid
+name" LOCATION '/no/such/location';
+
 -- triggers
 CREATE FUNCTION tf1() RETURNS trigger
 LANGUAGE plpgsql
index adac36536db4db137bd84f0f470db2c12ccdf182..70b342abeb665bab3980198cdd8ccef82c95fe83 100644 (file)
@@ -75,6 +75,8 @@ CREATE ROLE pg_abc; -- error
 CREATE ROLE "pg_abc"; -- error
 CREATE ROLE pg_abcdef; -- error
 CREATE ROLE "pg_abcdef"; -- error
+CREATE ROLE "invalid
+rolename"; -- error
 
 CREATE ROLE regress_testrol0 SUPERUSER LOGIN;
 CREATE ROLE regress_testrolx SUPERUSER LOGIN;
index a90e39e57382b945656b753d40df029c1be9750c..185880a3217a0a01409fb1e01dc45a7b6302d00a 100644 (file)
@@ -958,6 +958,11 @@ ALTER TABLE ALL IN TABLESPACE regress_tblspace_renamed SET TABLESPACE pg_default
 NOTICE:  no matching relations in tablespace "regress_tblspace_renamed" found
 ALTER MATERIALIZED VIEW ALL IN TABLESPACE regress_tblspace_renamed SET TABLESPACE pg_default;
 NOTICE:  no matching relations in tablespace "regress_tblspace_renamed" found
+-- Should fail, contains \n in name
+ALTER TABLESPACE regress_tblspace_renamed RENAME TO "invalid
+name";
+ERROR:  tablespace name "invalid
+name" contains a newline or carriage return character
 -- Should succeed
 DROP TABLESPACE regress_tblspace_renamed;
 DROP SCHEMA testschema CASCADE;
index dfe3db096e28dc16d13974740e4619af1dc9d3ec..c43a59e59573616553d5ab72747361df6368d197 100644 (file)
@@ -430,6 +430,10 @@ ALTER MATERIALIZED VIEW ALL IN TABLESPACE regress_tblspace_renamed SET TABLESPAC
 ALTER TABLE ALL IN TABLESPACE regress_tblspace_renamed SET TABLESPACE pg_default;
 ALTER MATERIALIZED VIEW ALL IN TABLESPACE regress_tblspace_renamed SET TABLESPACE pg_default;
 
+-- Should fail, contains \n in name
+ALTER TABLESPACE regress_tblspace_renamed RENAME TO "invalid
+name";
+
 -- Should succeed
 DROP TABLESPACE regress_tblspace_renamed;