mirror of
https://github.com/postgres/postgres.git
synced 2025-10-22 14:32:25 +03:00
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. Commitbc8cd50fef
(back-patched to v11+ in 2023-05 releases) introduced the pg_dump case. Commit6cbdbd9e8d
(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
This commit is contained in:
@@ -29,6 +29,43 @@ static void AddAcl(PQExpBuffer aclbuf, const char *keyword,
|
|||||||
const char *subname);
|
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.
|
* Build GRANT/REVOKE command(s) for an object.
|
||||||
*
|
*
|
||||||
|
@@ -36,6 +36,7 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
extern char *sanitize_line(const char *str, bool want_hyphen);
|
||||||
extern bool buildACLCommands(const char *name, const char *subname, const char *nspname,
|
extern bool buildACLCommands(const char *name, const char *subname, const char *nspname,
|
||||||
const char *type, const char *acls, const char *racls,
|
const char *type, const char *acls, const char *racls,
|
||||||
const char *owner, const char *prefix, int remoteVersion,
|
const char *owner, const char *prefix, int remoteVersion,
|
||||||
|
@@ -73,7 +73,6 @@ static ArchiveHandle *_allocAH(const char *FileSpec, const ArchiveFormat fmt,
|
|||||||
static void _getObjectDescription(PQExpBuffer buf, TocEntry *te,
|
static void _getObjectDescription(PQExpBuffer buf, TocEntry *te,
|
||||||
ArchiveHandle *AH);
|
ArchiveHandle *AH);
|
||||||
static void _printTocEntry(ArchiveHandle *AH, TocEntry *te, bool isData);
|
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 _doSetFixedOutputState(ArchiveHandle *AH);
|
||||||
static void _doSetSessionAuth(ArchiveHandle *AH, const char *user);
|
static void _doSetSessionAuth(ArchiveHandle *AH, const char *user);
|
||||||
static void _reconnectToDB(ArchiveHandle *AH, const char *dbname);
|
static void _reconnectToDB(ArchiveHandle *AH, const char *dbname);
|
||||||
@@ -3719,42 +3718,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
|
* Write the file header for a custom-format archive
|
||||||
*/
|
*/
|
||||||
|
@@ -2396,11 +2396,14 @@ dumpTableData(Archive *fout, TableDataInfo *tdinfo)
|
|||||||
forcePartitionRootLoad(tbinfo)))
|
forcePartitionRootLoad(tbinfo)))
|
||||||
{
|
{
|
||||||
TableInfo *parentTbinfo;
|
TableInfo *parentTbinfo;
|
||||||
|
char *sanitized;
|
||||||
|
|
||||||
parentTbinfo = getRootTableInfo(tbinfo);
|
parentTbinfo = getRootTableInfo(tbinfo);
|
||||||
copyFrom = fmtQualifiedDumpable(parentTbinfo);
|
copyFrom = fmtQualifiedDumpable(parentTbinfo);
|
||||||
|
sanitized = sanitize_line(copyFrom, true);
|
||||||
printfPQExpBuffer(copyBuf, "-- load via partition root %s",
|
printfPQExpBuffer(copyBuf, "-- load via partition root %s",
|
||||||
copyFrom);
|
sanitized);
|
||||||
|
free(sanitized);
|
||||||
tdDefn = pg_strdup(copyBuf->data);
|
tdDefn = pg_strdup(copyBuf->data);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@@ -1407,6 +1407,8 @@ dumpUserConfig(PGconn *conn, const char *username)
|
|||||||
if (PQntuples(res) == 1 &&
|
if (PQntuples(res) == 1 &&
|
||||||
!PQgetisnull(res, 0, 0))
|
!PQgetisnull(res, 0, 0))
|
||||||
{
|
{
|
||||||
|
char *sanitized;
|
||||||
|
|
||||||
/* comment at section start, only if needed */
|
/* comment at section start, only if needed */
|
||||||
if (first)
|
if (first)
|
||||||
{
|
{
|
||||||
@@ -1414,7 +1416,9 @@ dumpUserConfig(PGconn *conn, const char *username)
|
|||||||
first = false;
|
first = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
fprintf(OPF, "--\n-- User Config \"%s\"\n--\n\n", username);
|
sanitized = sanitize_line(username, true);
|
||||||
|
fprintf(OPF, "--\n-- User Config \"%s\"\n--\n\n", sanitized);
|
||||||
|
free(sanitized);
|
||||||
resetPQExpBuffer(buf);
|
resetPQExpBuffer(buf);
|
||||||
makeAlterConfigCommand(conn, PQgetvalue(res, 0, 0),
|
makeAlterConfigCommand(conn, PQgetvalue(res, 0, 0),
|
||||||
"ROLE", username, NULL, NULL,
|
"ROLE", username, NULL, NULL,
|
||||||
@@ -1508,6 +1512,7 @@ dumpDatabases(PGconn *conn)
|
|||||||
for (i = 0; i < PQntuples(res); i++)
|
for (i = 0; i < PQntuples(res); i++)
|
||||||
{
|
{
|
||||||
char *dbname = PQgetvalue(res, i, 0);
|
char *dbname = PQgetvalue(res, i, 0);
|
||||||
|
char *sanitized;
|
||||||
const char *create_opts;
|
const char *create_opts;
|
||||||
int ret;
|
int ret;
|
||||||
|
|
||||||
@@ -1524,7 +1529,9 @@ dumpDatabases(PGconn *conn)
|
|||||||
|
|
||||||
pg_log_info("dumping database \"%s\"", dbname);
|
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
|
* We assume that "template1" and "postgres" already exist in the
|
||||||
|
@@ -1430,6 +1430,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 DATABASE regression_invalid...' => {
|
||||||
create_order => 1,
|
create_order => 1,
|
||||||
create_sql => q(
|
create_sql => q(
|
||||||
|
@@ -3,7 +3,7 @@ use warnings;
|
|||||||
|
|
||||||
use PostgresNode;
|
use PostgresNode;
|
||||||
use TestLib;
|
use TestLib;
|
||||||
use Test::More tests => 3;
|
use Test::More tests => 6;
|
||||||
|
|
||||||
my $tempdir = TestLib::tempdir;
|
my $tempdir = TestLib::tempdir;
|
||||||
my $tempdir_short = TestLib::tempdir_short;
|
my $tempdir_short = TestLib::tempdir_short;
|
||||||
@@ -14,6 +14,22 @@ my $port = $node->port;
|
|||||||
$node->init;
|
$node->init;
|
||||||
$node->start;
|
$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
|
# Verify that dumping foreign data includes only foreign tables of
|
||||||
# matching servers
|
# matching servers
|
||||||
@@ -24,7 +40,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 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 t0 (a int) SERVER s0");
|
||||||
$node->safe_psql('postgres', "CREATE FOREIGN TABLE t1 (a int) SERVER s1");
|
$node->safe_psql('postgres', "CREATE FOREIGN TABLE t1 (a int) SERVER s1");
|
||||||
my ($cmd, $stdout, $stderr, $result);
|
|
||||||
|
|
||||||
command_fails_like(
|
command_fails_like(
|
||||||
[ "pg_dump", '-p', $port, '--include-foreign-data=s0', 'postgres' ],
|
[ "pg_dump", '-p', $port, '--include-foreign-data=s0', 'postgres' ],
|
||||||
|
Reference in New Issue
Block a user