Convert newlines to spaces in names written in v11+ pg_dump comments.

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
bc8cd50fef (back-patched to v11+ in
2023-05 releases) introduced the pg_dump case.  Commit
6cbdbd9e8d (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
REL_16_STABLE
Noah Misch 1 month ago
parent 1926342923
commit 850caae60c
  1. 37
      src/bin/pg_dump/dumputils.c
  2. 1
      src/bin/pg_dump/dumputils.h
  3. 37
      src/bin/pg_dump/pg_backup_archiver.c
  4. 5
      src/bin/pg_dump/pg_dump.c
  5. 13
      src/bin/pg_dump/pg_dumpall.c
  6. 21
      src/bin/pg_dump/t/002_pg_dump.pl
  7. 17
      src/bin/pg_dump/t/003_pg_dump_with_server.pl

@ -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.
*

@ -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,

@ -69,7 +69,6 @@ static ArchiveHandle *_allocAH(const char *FileSpec, const ArchiveFormat fmt,
SetupWorkerPtrType setupWorkerPtr);
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);
@ -3684,42 +3683,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
*/

@ -2609,11 +2609,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

@ -1447,7 +1447,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++)
{
@ -1549,6 +1555,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;
@ -1565,7 +1572,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

@ -1907,6 +1907,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 DATABASE regression_invalid...' => {
create_order => 1,
create_sql => q(

@ -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' ],

Loading…
Cancel
Save