Convert newlines to spaces in names written in v11+ pg_dump comments.
authorNoah Misch <noah@leadboat.com>
Mon, 11 Aug 2025 13:18:59 +0000 (06:18 -0700)
committerNoah Misch <noah@leadboat.com>
Mon, 11 Aug 2025 13:19:03 +0000 (06:19 -0700)
Maliciously-crafted object names could achieve SQL injection during
restore.  CVE-2012-0868 fixed this class of problem at the time, but
later work reintroduced three cases.  Commit
bc8cd50fefd369b217f80078585c486505aafb62 (back-patched to v11+ in
2023-05 releases) introduced the pg_dump case.  Commit
6cbdbd9e8d8f2986fde44f2431ed8d0c8fce7f5d (v12+) introduced the two
pg_dumpall cases.  Move sanitize_line(), unchanged, to dumputils.c so
pg_dumpall has access to it in all supported versions.  Back-patch to
v13 (all supported versions).

Reviewed-by: Robert Haas <robertmhaas@gmail.com>
Reviewed-by: Nathan Bossart <nathandbossart@gmail.com>
Backpatch-through: 13
Security: CVE-2025-8715

src/bin/pg_dump/dumputils.c
src/bin/pg_dump/dumputils.h
src/bin/pg_dump/pg_backup_archiver.c
src/bin/pg_dump/pg_dump.c
src/bin/pg_dump/pg_dumpall.c
src/bin/pg_dump/t/002_pg_dump.pl
src/bin/pg_dump/t/003_pg_dump_with_server.pl

index 5649859aa1e2d0402c041053e1f3d241533be0f1..9e5748311e0472cb7f68f40c5c4db05a99f4fc76 100644 (file)
@@ -29,6 +29,43 @@ static void AddAcl(PQExpBuffer aclbuf, const char *keyword,
                   const char *subname);
 
 
+/*
+ * Sanitize a string to be included in an SQL comment or TOC listing, by
+ * replacing any newlines with spaces.  This ensures each logical output line
+ * is in fact one physical output line, to prevent corruption of the dump
+ * (which could, in the worst case, present an SQL injection vulnerability
+ * if someone were to incautiously load a dump containing objects with
+ * maliciously crafted names).
+ *
+ * The result is a freshly malloc'd string.  If the input string is NULL,
+ * return a malloc'ed empty string, unless want_hyphen, in which case return a
+ * malloc'ed hyphen.
+ *
+ * Note that we currently don't bother to quote names, meaning that the name
+ * fields aren't automatically parseable.  "pg_restore -L" doesn't care because
+ * it only examines the dumpId field, but someday we might want to try harder.
+ */
+char *
+sanitize_line(const char *str, bool want_hyphen)
+{
+   char       *result;
+   char       *s;
+
+   if (!str)
+       return pg_strdup(want_hyphen ? "-" : "");
+
+   result = pg_strdup(str);
+
+   for (s = result; *s != '\0'; s++)
+   {
+       if (*s == '\n' || *s == '\r')
+           *s = ' ';
+   }
+
+   return result;
+}
+
+
 /*
  * Build GRANT/REVOKE command(s) for an object.
  *
index 98c71c6bf947335bea5010e2fce8cd213a3fd8b6..d1248d9061ba1342aedb8d92cd0481b9edfcc60e 100644 (file)
@@ -36,6 +36,7 @@
 #endif
 
 
+extern char *sanitize_line(const char *str, bool want_hyphen);
 extern bool buildACLCommands(const char *name, const char *subname, const char *nspname,
                             const char *type, const char *acls, const char *baseacls,
                             const char *owner, const char *prefix, int remoteVersion,
index f1ffed038b046b5262756b87143ac6ef2fcd4744..9d0b5d0bd7f87010c12599e6ea330d41c271a5f6 100644 (file)
@@ -54,7 +54,6 @@ static ArchiveHandle *_allocAH(const char *FileSpec, const ArchiveFormat fmt,
                               DataDirSyncMethod sync_method);
 static void _getObjectDescription(PQExpBuffer buf, const TocEntry *te);
 static void _printTocEntry(ArchiveHandle *AH, TocEntry *te, bool isData);
-static char *sanitize_line(const char *str, bool want_hyphen);
 static void _doSetFixedOutputState(ArchiveHandle *AH);
 static void _doSetSessionAuth(ArchiveHandle *AH, const char *user);
 static void _reconnectToDB(ArchiveHandle *AH, const char *dbname);
@@ -3915,42 +3914,6 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, bool isData)
    }
 }
 
-/*
- * Sanitize a string to be included in an SQL comment or TOC listing, by
- * replacing any newlines with spaces.  This ensures each logical output line
- * is in fact one physical output line, to prevent corruption of the dump
- * (which could, in the worst case, present an SQL injection vulnerability
- * if someone were to incautiously load a dump containing objects with
- * maliciously crafted names).
- *
- * The result is a freshly malloc'd string.  If the input string is NULL,
- * return a malloc'ed empty string, unless want_hyphen, in which case return a
- * malloc'ed hyphen.
- *
- * Note that we currently don't bother to quote names, meaning that the name
- * fields aren't automatically parseable.  "pg_restore -L" doesn't care because
- * it only examines the dumpId field, but someday we might want to try harder.
- */
-static char *
-sanitize_line(const char *str, bool want_hyphen)
-{
-   char       *result;
-   char       *s;
-
-   if (!str)
-       return pg_strdup(want_hyphen ? "-" : "");
-
-   result = pg_strdup(str);
-
-   for (s = result; *s != '\0'; s++)
-   {
-       if (*s == '\n' || *s == '\r')
-           *s = ' ';
-   }
-
-   return result;
-}
-
 /*
  * Write the file header for a custom-format archive
  */
index 0f26b018d4afc5b2e2034b929e1f0be0eaa11285..201b17697a410ffcfc7f7130691acc812673da57 100644 (file)
@@ -2657,11 +2657,14 @@ dumpTableData(Archive *fout, const TableDataInfo *tdinfo)
         forcePartitionRootLoad(tbinfo)))
    {
        TableInfo  *parentTbinfo;
+       char       *sanitized;
 
        parentTbinfo = getRootTableInfo(tbinfo);
        copyFrom = fmtQualifiedDumpable(parentTbinfo);
+       sanitized = sanitize_line(copyFrom, true);
        printfPQExpBuffer(copyBuf, "-- load via partition root %s",
-                         copyFrom);
+                         sanitized);
+       free(sanitized);
        tdDefn = pg_strdup(copyBuf->data);
    }
    else
index d5282ecb17a1ad9d2cae80cfcc06e091cd3e8eae..1f98c89c34a70a951be7e72a64210ab70e284f44 100644 (file)
@@ -1462,7 +1462,13 @@ dumpUserConfig(PGconn *conn, const char *username)
    res = executeQuery(conn, buf->data);
 
    if (PQntuples(res) > 0)
-       fprintf(OPF, "\n--\n-- User Config \"%s\"\n--\n\n", username);
+   {
+       char       *sanitized;
+
+       sanitized = sanitize_line(username, true);
+       fprintf(OPF, "\n--\n-- User Config \"%s\"\n--\n\n", sanitized);
+       free(sanitized);
+   }
 
    for (int i = 0; i < PQntuples(res); i++)
    {
@@ -1564,6 +1570,7 @@ dumpDatabases(PGconn *conn)
    for (i = 0; i < PQntuples(res); i++)
    {
        char       *dbname = PQgetvalue(res, i, 0);
+       char       *sanitized;
        const char *create_opts;
        int         ret;
 
@@ -1580,7 +1587,9 @@ dumpDatabases(PGconn *conn)
 
        pg_log_info("dumping database \"%s\"", dbname);
 
-       fprintf(OPF, "--\n-- Database \"%s\" dump\n--\n\n", dbname);
+       sanitized = sanitize_line(dbname, true);
+       fprintf(OPF, "--\n-- Database \"%s\" dump\n--\n\n", sanitized);
+       free(sanitized);
 
        /*
         * We assume that "template1" and "postgres" already exist in the
index 58306d307eceea2477bfc0bce2c26e6e98d86810..5e256b52d24e672375f8e74193db8bda4cc0245f 100644 (file)
@@ -1893,6 +1893,27 @@ 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
+                        CREATE TYPE pp_colors AS
+                           ENUM ('green', 'blue', 'black');
+                        CREATE TABLE pp_enumpart (a pp_colors)
+                           PARTITION BY HASH (a);
+                        CREATE TABLE pp_enumpart1 PARTITION OF pp_enumpart
+                           FOR VALUES WITH (MODULUS 2, REMAINDER 0);
+                        CREATE TABLE pp_enumpart2 PARTITION OF pp_enumpart
+                           FOR VALUES WITH (MODULUS 2, REMAINDER 1);
+                        ALTER TABLE pp_enumpart
+                           RENAME TO "pp_enumpart\nattack";},
+       regexp => qr/\n--[^\n]*\nattack/s,
+       like => {},
+   },
+
    'CREATE TABLESPACE regress_dump_tablespace' => {
        create_order => 2,
        create_sql => q(
index b5a144555074db3c7686610da2868a4a2e033715..6948dcd2984212f9311b8b9c231cc010edbdc123 100644 (file)
@@ -16,6 +16,22 @@ 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
@@ -26,7 +42,6 @@ $node->safe_psql('postgres', "CREATE SERVER s1 FOREIGN DATA WRAPPER dummy");
 $node->safe_psql('postgres', "CREATE SERVER s2 FOREIGN DATA WRAPPER dummy");
 $node->safe_psql('postgres', "CREATE FOREIGN TABLE t0 (a int) SERVER s0");
 $node->safe_psql('postgres', "CREATE FOREIGN TABLE t1 (a int) SERVER s1");
-my ($cmd, $stdout, $stderr, $result);
 
 command_fails_like(
    [ "pg_dump", '-p', $port, '--include-foreign-data=s0', 'postgres' ],