mirror of
				https://github.com/postgres/postgres.git
				synced 2025-10-31 10:30:33 +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 *baseacls, | 							 const char *type, const char *acls, const char *baseacls, | ||||||
| 							 const char *owner, const char *prefix, int remoteVersion, | 							 const char *owner, const char *prefix, int remoteVersion, | ||||||
|   | |||||||
| @@ -54,7 +54,6 @@ static ArchiveHandle *_allocAH(const char *FileSpec, const ArchiveFormat fmt, | |||||||
| 							   DataDirSyncMethod sync_method); | 							   DataDirSyncMethod sync_method); | ||||||
| static void _getObjectDescription(PQExpBuffer buf, const TocEntry *te); | static void _getObjectDescription(PQExpBuffer buf, const TocEntry *te); | ||||||
| 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); | ||||||
| @@ -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 |  * Write the file header for a custom-format archive | ||||||
|  */ |  */ | ||||||
|   | |||||||
| @@ -2657,11 +2657,14 @@ dumpTableData(Archive *fout, const 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 | ||||||
|   | |||||||
| @@ -1462,7 +1462,13 @@ dumpUserConfig(PGconn *conn, const char *username) | |||||||
| 	res = executeQuery(conn, buf->data); | 	res = executeQuery(conn, buf->data); | ||||||
|  |  | ||||||
| 	if (PQntuples(res) > 0) | 	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++) | 	for (int i = 0; i < PQntuples(res); i++) | ||||||
| 	{ | 	{ | ||||||
| @@ -1564,6 +1570,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; | ||||||
|  |  | ||||||
| @@ -1580,7 +1587,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 | ||||||
|   | |||||||
| @@ -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 TABLESPACE regress_dump_tablespace' => { | ||||||
| 		create_order => 2, | 		create_order => 2, | ||||||
| 		create_sql => q( | 		create_sql => q( | ||||||
|   | |||||||
| @@ -16,6 +16,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 | ||||||
| @@ -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 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