From df245c37458315cf8a6ff08f4f8c3b1103857ed3 Mon Sep 17 00:00:00 2001 From: Nathan Bossart Date: Mon, 10 Aug 2026 06:38:31 -0700 Subject: [PATCH] psql: Don't do backquote expansion in \unrestrict. This oversight in commit 71ea0d6795 allows a malicious server to inject shell commands into plain-text dump output that are run at restore time on the machine running psql. To fix, interpret all text after \unrestrict until the end of the line as its argument. Reported-by: Lucas Velgus Reported-by: Filip Janus Reported-by: Daniel Bakker Author: Nathan Bossart Reviewed-by: Robert Haas Reviewed-by: Noah Misch Security: CVE-2026-18408 Backpatch-through: 14 --- doc/src/sgml/ref/psql-ref.sgml | 5 +++++ src/bin/psql/command.c | 19 +++++++++++++++++-- src/bin/psql/t/001_basic.pl | 7 +++++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index a8b00c10162..81cf910457c 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -3451,6 +3451,11 @@ testdb=> \setenv LESS -imx4F pg_dumpall, and pg_restore, but it may be useful elsewhere. + + Unlike most other meta-commands, the entire remainder of the line is + always taken to be the argument of \unrestrict, and + neither variable interpolation nor backquote expansion are performed. + diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c index 3366013b435..ae3ab3295d5 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -2349,6 +2349,12 @@ exec_command_restrict(PsqlScanState scan_state, bool active_branch, Assert(!restricted); + /* + * Unlike \unrestrict, this argument may safely undergo backquote and + * variable expansion: HandleSlashCmds() rejects \restrict in + * restricted mode before its argument is scanned, so we only get here + * when the input could execute such things anyway. + */ opt = psql_scan_slash_option(scan_state, OT_NORMAL, NULL, true); if (opt == NULL || opt[0] == '\0') { @@ -2663,14 +2669,23 @@ exec_command_unrestrict(PsqlScanState scan_state, bool active_branch, if (active_branch) { char *opt; + size_t len; - opt = psql_scan_slash_option(scan_state, OT_NORMAL, NULL, true); + opt = psql_scan_slash_option(scan_state, OT_WHOLE_LINE, NULL, true); if (opt == NULL || opt[0] == '\0') { pg_log_error("\\%s: missing required argument", cmd); return PSQL_CMD_ERROR; } + /* strip any trailing spaces and semicolons */ + len = strlen(opt); + while (len > 0 && + (opt[len - 1] == ';' || + (isascii((unsigned char) opt[len - 1]) && + isspace((unsigned char) opt[len - 1])))) + opt[--len] = '\0'; + if (!restricted) { pg_log_error("\\%s: not currently in restricted mode", cmd); @@ -2688,7 +2703,7 @@ exec_command_unrestrict(PsqlScanState scan_state, bool active_branch, } } else - ignore_slash_options(scan_state); + ignore_slash_whole_line(scan_state); return PSQL_CMD_SKIP_LINE; } diff --git a/src/bin/psql/t/001_basic.pl b/src/bin/psql/t/001_basic.pl index ef5ec24355a..660c9ab4fac 100644 --- a/src/bin/psql/t/001_basic.pl +++ b/src/bin/psql/t/001_basic.pl @@ -332,4 +332,11 @@ psql_fails_like( qr/backslash commands are restricted; only \\unrestrict is allowed/, 'meta-command in restrict mode fails'); +psql_fails_like( + $node, + qq{\\restrict test +\\unrestrict `echo test`}, + qr/wrong key/, + '\unrestrict does not do backquote expansion'); + done_testing();