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); | ||||
|  | ||||
|  | ||||
| /* | ||||
|  * 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, | ||||
|   | ||||
| @@ -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 | ||||
|  */ | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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( | ||||
|   | ||||
| @@ -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' ], | ||||
|   | ||||
		Reference in New Issue
	
	Block a user