From 4b0d1c5f9ea112306c02cb0cc6a636bd9b10f98d Mon Sep 17 00:00:00 2001 From: Robert Haas Date: Fri, 24 Jul 2026 08:14:15 -0400 Subject: [PATCH v1 4/8] stringinfo: Add helpers for shell-quoting. Commit XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX fixed various cases of improper shell escaping, but there are other cases that cannot be fixed easily using that method, because PQExpBuffer is only available to code that depends on libpq. Hence, add shell-quoting helpers for StringInfo, which is available to both backend and frontend code since commit 26aaf97b683d6258c098859e6b1268e1f5da242f. These are almost identical to the existing helpers, which took their current form after commit 41f18f021a0882eccbeca62e2ed4b66c6b96e9c9 promoted the pre-existing pg_dumpall helpers to src/fe_utils. In addition to being useful for fixing the existing set of problems, these helpers seem like they have a good chance of being useful in new code, since StringInfo has become a popular choice for new frontend code. Perhaps we'll even want to think of porting some existing PQExpBuffer usage to StringInfo, but that's a question for another day. Backpatch-through: 14 --- src/common/stringinfo.c | 134 +++++++++++++++++++++++++++++++++++ src/fe_utils/string_utils.c | 3 + src/include/lib/stringinfo.h | 14 ++++ 3 files changed, 151 insertions(+) diff --git a/src/common/stringinfo.c b/src/common/stringinfo.c index 468954e484e..9d9dc3000ee 100644 --- a/src/common/stringinfo.c +++ b/src/common/stringinfo.c @@ -271,6 +271,140 @@ appendStringInfoSpaces(StringInfo str, int count) } } +/* + * Append the given string to the shell command being built in str, with + * shell-style quoting as needed to create exactly one argument. + * + * Forbid LF or CR characters, which have scant practical use beyond designing + * security breaches. The Windows command shell is unusable as a conduit for + * arguments containing LF or CR characters. + * + * appendStringInfoShell() reports an error and does not return if LF or CR + * appears; in the backend it does ereport(ERROR), while in frontend code it + * prints a message and exits. appendStringInfoShellNoError() omits those + * characters from the result, and returns false if there were any. + * + * If you make any changes here, also update appendShellString and + * appendShellStringNoError. + */ +void +appendStringInfoShell(StringInfo str, const char *s) +{ + if (!appendStringInfoShellNoError(str, s)) + { +#ifndef FRONTEND + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("shell command argument contains a newline or carriage return: \"%s\"", + s))); +#else + fprintf(stderr, + _("shell command argument contains a newline or carriage return: \"%s\"\n"), + s); + exit(EXIT_FAILURE); +#endif + } +} + +bool +appendStringInfoShellNoError(StringInfo str, const char *s) +{ +#ifdef WIN32 + int backslash_run_length = 0; +#endif + bool ok = true; + const char *p; + + /* + * Don't bother with adding quotes if the string is nonempty and clearly + * contains only safe characters. + */ + if (*s != '\0' && + strspn(s, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_./:") == strlen(s)) + { + appendStringInfoString(str, s); + return ok; + } + +#ifndef WIN32 + appendStringInfoChar(str, '\''); + for (p = s; *p; p++) + { + if (*p == '\n' || *p == '\r') + { + ok = false; + continue; + } + + if (*p == '\'') + appendStringInfoString(str, "'\"'\"'"); + else + appendStringInfoChar(str, *p); + } + appendStringInfoChar(str, '\''); +#else /* WIN32 */ + + /* + * A Windows system() argument experiences two layers of interpretation. + * First, cmd.exe interprets the string. Its behavior is undocumented, + * but a caret escapes any byte except LF or CR that would otherwise have + * special meaning. Handling of a caret before LF or CR differs between + * "cmd.exe /c" and other modes, and it is unusable here. + * + * Second, the new process parses its command line to construct argv (see + * https://msdn.microsoft.com/en-us/library/17w5ykft.aspx). This treats + * backslash-double quote sequences specially. + */ + appendStringInfoString(str, "^\""); + for (p = s; *p; p++) + { + if (*p == '\n' || *p == '\r') + { + ok = false; + continue; + } + + /* Change N backslashes before a double quote to 2N+1 backslashes. */ + if (*p == '"') + { + while (backslash_run_length) + { + appendStringInfoString(str, "^\\"); + backslash_run_length--; + } + appendStringInfoString(str, "^\\"); + } + else if (*p == '\\') + backslash_run_length++; + else + backslash_run_length = 0; + + /* + * Decline to caret-escape the most mundane characters, to ease + * debugging and lest we approach the command length limit. + */ + if (!((*p >= 'a' && *p <= 'z') || + (*p >= 'A' && *p <= 'Z') || + (*p >= '0' && *p <= '9'))) + appendStringInfoChar(str, '^'); + appendStringInfoChar(str, *p); + } + + /* + * Change N backslashes at end of argument to 2N backslashes, because they + * precede the double quote that terminates the argument. + */ + while (backslash_run_length) + { + appendStringInfoString(str, "^\\"); + backslash_run_length--; + } + appendStringInfoString(str, "^\""); +#endif /* WIN32 */ + + return ok; +} + /* * appendBinaryStringInfo * diff --git a/src/fe_utils/string_utils.c b/src/fe_utils/string_utils.c index 7a762251f32..50d83ec2501 100644 --- a/src/fe_utils/string_utils.c +++ b/src/fe_utils/string_utils.c @@ -575,6 +575,9 @@ appendByteaLiteral(PQExpBuffer buf, const unsigned char *str, size_t length, * appendShellString() simply prints an error and dies if LF or CR appears. * appendShellStringNoError() omits those characters from the result, and * returns false if there were any. + * + * If you make any changes here, also update appendStringInfoShell and + * appendStringInfoShellNoError. */ void appendShellString(PQExpBuffer buf, const char *str) diff --git a/src/include/lib/stringinfo.h b/src/include/lib/stringinfo.h index e3f4b922300..a5a16021552 100644 --- a/src/include/lib/stringinfo.h +++ b/src/include/lib/stringinfo.h @@ -239,6 +239,20 @@ extern void appendStringInfoChar(StringInfo str, char ch); */ extern void appendStringInfoSpaces(StringInfo str, int count); +/*------------------------ + * appendStringInfoShell + * appendStringInfoShellNoError + * + * Append a string to str, escaping it with shell-style quoting as needed to + * create exactly one argument. If the input contains a CR or LF character, + * appendStringInfoShell will ereport(ERROR, ...) in backend code or fprintf + + * exit in frontend code. If a non-local transfer of control is undesirable, + * appendStringInfoShellNoError can be used; it will omit such characters from + * the output and return false if any are present. + */ +extern void appendStringInfoShell(StringInfo str, const char *s); +extern bool appendStringInfoShellNoError(StringInfo str, const char *s); + /*------------------------ * appendBinaryStringInfo * Append arbitrary binary data to a StringInfo, allocating more space -- 2.50.1 (Apple Git-155)