From c204bd39f7b57284363390f50e81abfc350a0fb9 Mon Sep 17 00:00:00 2001 From: Nathan Bossart Date: Mon, 11 Aug 2025 09:00:00 -0500 Subject: [PATCH] Restrict psql meta-commands in plain-text dumps. A malicious server could inject psql meta-commands into plain-text dump output (i.e., scripts created with pg_dump --format=plain, pg_dumpall, or pg_restore --file) that are run at restore time on the machine running psql. To fix, introduce a new "restricted" mode in psql that blocks all meta-commands (except for \unrestrict to exit the mode), and teach pg_dump, pg_dumpall, and pg_restore to use this mode in plain-text dumps. While at it, encourage users to only restore dumps generated from trusted servers or to inspect it beforehand, since restoring causes the destination to execute arbitrary code of the source superusers' choice. However, the client running the dump and restore needn't trust the source or destination superusers. Reported-by: Martin Rakhmanov Reported-by: Matthieu Denais Reported-by: RyotaK Suggested-by: Tom Lane Reviewed-by: Noah Misch Reviewed-by: Michael Paquier Reviewed-by: Peter Eisentraut Security: CVE-2025-8714 Backpatch-through: 13 --- doc/src/sgml/ref/pg_dump.sgml | 35 +++++++++++ doc/src/sgml/ref/pg_dumpall.sgml | 30 +++++++++ doc/src/sgml/ref/pg_restore.sgml | 34 ++++++++++ doc/src/sgml/ref/pgupgrade.sgml | 8 +++ doc/src/sgml/ref/psql-ref.sgml | 36 +++++++++++ src/bin/pg_dump/dumputils.c | 38 +++++++++++ src/bin/pg_dump/dumputils.h | 3 + src/bin/pg_dump/pg_backup.h | 4 ++ src/bin/pg_dump/pg_backup_archiver.c | 32 +++++++++- src/bin/pg_dump/pg_dump.c | 21 +++++++ src/bin/pg_dump/pg_dumpall.c | 42 +++++++++++++ src/bin/pg_dump/pg_restore.c | 31 +++++++++ src/bin/pg_dump/t/002_pg_dump.pl | 19 ++++-- src/bin/pg_upgrade/test.sh | 2 + src/bin/psql/command.c | 94 +++++++++++++++++++++++++++- src/bin/psql/help.c | 4 ++ src/bin/psql/tab-complete.c | 4 +- src/test/regress/expected/psql.out | 2 + src/test/regress/sql/psql.sql | 2 + 19 files changed, 431 insertions(+), 10 deletions(-) diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml index 0ad307592fd..19c7006b733 100644 --- a/doc/src/sgml/ref/pg_dump.sgml +++ b/doc/src/sgml/ref/pg_dump.sgml @@ -92,6 +92,18 @@ PostgreSQL documentation light of the limitations listed below. + + + Restoring a dump causes the destination to execute arbitrary code of the + source superusers' choice. Partial dumps and partial restores do not limit + that. If the source superusers are not trusted, the dumped SQL statements + must be inspected before restoring. Non-plain-text dumps can be inspected + by using pg_restore's + option. Note that the client running the dump and restore need not trust + the source or destination superusers. + + + @@ -984,6 +996,29 @@ PostgreSQL documentation + + + + + Use the provided string as the psql + \restrict key in the dump output. This can only be + specified for plain-text dumps, i.e., when is + set to plain or the option + is omitted. If no restrict key is specified, + pg_dump will generate a random one as + needed. Keys may contain only alphanumeric characters. + + + This option is primarily intended for testing purposes and other + scenarios that require repeatable output (e.g., comparing dump files). + It is not recommended for general use, as a malicious server with + advance knowledge of the key may be able to inject arbitrary code that + will be executed on the machine that runs + psql with the dump output. + + + + diff --git a/doc/src/sgml/ref/pg_dumpall.sgml b/doc/src/sgml/ref/pg_dumpall.sgml index e629c1d0469..70d94c29cf9 100644 --- a/doc/src/sgml/ref/pg_dumpall.sgml +++ b/doc/src/sgml/ref/pg_dumpall.sgml @@ -65,6 +65,16 @@ PostgreSQL documentation linkend="libpq-pgpass"/> for more information. + + + Restoring a dump causes the destination to execute arbitrary code of the + source superusers' choice. Partial dumps and partial restores do not limit + that. If the source superusers are not trusted, the dumped SQL statements + must be inspected before restoring. Note that the client running the dump + and restore need not trust the source or destination superusers. + + + @@ -501,6 +511,26 @@ PostgreSQL documentation + + + + + Use the provided string as the psql + \restrict key in the dump output. If no restrict + key is specified, pg_dumpall will generate a + random one as needed. Keys may contain only alphanumeric characters. + + + This option is primarily intended for testing purposes and other + scenarios that require repeatable output (e.g., comparing dump files). + It is not recommended for general use, as a malicious server with + advance knowledge of the key may be able to inject arbitrary code that + will be executed on the machine that runs + psql with the dump output. + + + + diff --git a/doc/src/sgml/ref/pg_restore.sgml b/doc/src/sgml/ref/pg_restore.sgml index f8e7351b82b..9e54a2f545a 100644 --- a/doc/src/sgml/ref/pg_restore.sgml +++ b/doc/src/sgml/ref/pg_restore.sgml @@ -68,6 +68,18 @@ PostgreSQL documentation pg_restore will not be able to load the data using COPY statements. + + + + Restoring a dump causes the destination to execute arbitrary code of the + source superusers' choice. Partial dumps and partial restores do not limit + that. If the source superusers are not trusted, the dumped SQL statements + must be inspected before restoring. Non-plain-text dumps can be inspected + by using pg_restore's + option. Note that the client running the dump and restore need not trust + the source or destination superusers. + + @@ -659,6 +671,28 @@ PostgreSQL documentation + + + + + Use the provided string as the psql + \restrict key in the dump output. This can only be + specified for SQL script output, i.e., when the + option is used. If no restrict key is specified, + pg_restore will generate a random one as + needed. Keys may contain only alphanumeric characters. + + + This option is primarily intended for testing purposes and other + scenarios that require repeatable output (e.g., comparing dump files). + It is not recommended for general use, as a malicious server with + advance knowledge of the key may be able to inject arbitrary code that + will be executed on the machine that runs + psql with the dump output. + + + + diff --git a/doc/src/sgml/ref/pgupgrade.sgml b/doc/src/sgml/ref/pgupgrade.sgml index dfa61e95748..640176f4d3e 100644 --- a/doc/src/sgml/ref/pgupgrade.sgml +++ b/doc/src/sgml/ref/pgupgrade.sgml @@ -70,6 +70,14 @@ PostgreSQL documentation pg_upgrade supports upgrades from 9.0.X and later to the current major release of PostgreSQL, including snapshot and beta releases. + + + + Upgrading a cluster causes the destination to execute arbitrary code of the + source superusers' choice. Ensure that the source superusers are trusted + before upgrading. + + diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 97a18a5196f..50b879292bf 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -3147,6 +3147,24 @@ lo_import 152801 + + \restrict restrict_key + + + Enter "restricted" mode with the provided key. In this mode, the only + allowed meta-command is \unrestrict, to exit + restricted mode. The key may contain only alphanumeric characters. + + + This command is primarily intended for use in plain-text dumps + generated by pg_dump, + pg_dumpall, and + pg_restore, but it may be useful elsewhere. + + + + + \s [ filename ] @@ -3321,6 +3339,24 @@ testdb=> \setenv LESS -imx4F + + \unrestrict restrict_key + + + Exit "restricted" mode (i.e., where all other meta-commands are + blocked), provided the specified key matches the one given to + \restrict when restricted mode was entered. + + + This command is primarily intended for use in plain-text dumps + generated by pg_dump, + pg_dumpall, and + pg_restore, but it may be useful elsewhere. + + + + + \unset name diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c index 19ed4842f18..51067690685 100644 --- a/src/bin/pg_dump/dumputils.c +++ b/src/bin/pg_dump/dumputils.c @@ -19,6 +19,7 @@ #include "dumputils.h" #include "fe_utils/string_utils.h" +static const char restrict_chars[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; static bool parseAclItem(const char *item, const char *type, const char *name, const char *subname, int remoteVersion, @@ -1079,3 +1080,40 @@ makeAlterConfigCommand(PGconn *conn, const char *configitem, pg_free(mine); } + +/* + * Generates a valid restrict key (i.e., an alphanumeric string) for use with + * psql's \restrict and \unrestrict meta-commands. For safety, the value is + * chosen at random. + */ +char * +generate_restrict_key(void) +{ + uint8 buf[64]; + char *ret = palloc(sizeof(buf)); + + if (!pg_strong_random(buf, sizeof(buf))) + return NULL; + + for (int i = 0; i < sizeof(buf) - 1; i++) + { + uint8 idx = buf[i] % strlen(restrict_chars); + + ret[i] = restrict_chars[idx]; + } + ret[sizeof(buf) - 1] = '\0'; + + return ret; +} + +/* + * Checks that a given restrict key (intended for use with psql's \restrict and + * \unrestrict meta-commands) contains only alphanumeric characters. + */ +bool +valid_restrict_key(const char *restrict_key) +{ + return restrict_key != NULL && + restrict_key[0] != '\0' && + strspn(restrict_key, restrict_chars) == strlen(restrict_key); +} diff --git a/src/bin/pg_dump/dumputils.h b/src/bin/pg_dump/dumputils.h index 950cdb5f579..ddc28c2f3aa 100644 --- a/src/bin/pg_dump/dumputils.h +++ b/src/bin/pg_dump/dumputils.h @@ -67,4 +67,7 @@ extern void makeAlterConfigCommand(PGconn *conn, const char *configitem, const char *type2, const char *name2, PQExpBuffer buf); +extern char *generate_restrict_key(void); +extern bool valid_restrict_key(const char *restrict_key); + #endif /* DUMPUTILS_H */ diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h index 4c4e968a2fd..05aecd29ecb 100644 --- a/src/bin/pg_dump/pg_backup.h +++ b/src/bin/pg_dump/pg_backup.h @@ -134,6 +134,8 @@ typedef struct _restoreOptions int enable_row_security; int sequence_data; /* dump sequence data even in schema-only mode */ int binary_upgrade; + + char *restrict_key; } RestoreOptions; typedef struct _dumpOptions @@ -179,6 +181,8 @@ typedef struct _dumpOptions int sequence_data; /* dump sequence data even in schema-only mode */ int do_nothing; + + char *restrict_key; } DumpOptions; /* diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c index 3bbd3cb8880..3f62166055b 100644 --- a/src/bin/pg_dump/pg_backup_archiver.c +++ b/src/bin/pg_dump/pg_backup_archiver.c @@ -207,6 +207,7 @@ dumpOptionsFromRestoreOptions(RestoreOptions *ropt) dopt->include_everything = ropt->include_everything; dopt->enable_row_security = ropt->enable_row_security; dopt->sequence_data = ropt->sequence_data; + dopt->restrict_key = ropt->restrict_key ? pg_strdup(ropt->restrict_key) : NULL; return dopt; } @@ -463,6 +464,17 @@ RestoreArchive(Archive *AHX) ahprintf(AH, "--\n-- PostgreSQL database dump\n--\n\n"); + /* + * If generating plain-text output, enter restricted mode to block any + * unexpected psql meta-commands. A malicious source might try to inject + * a variety of things via bogus responses to queries. While we cannot + * prevent such sources from affecting the destination at restore time, we + * can block psql meta-commands so that the client machine that runs psql + * with the dump output remains unaffected. + */ + if (ropt->restrict_key) + ahprintf(AH, "\\restrict %s\n\n", ropt->restrict_key); + if (AH->archiveRemoteVersion) ahprintf(AH, "-- Dumped from database version %s\n", AH->archiveRemoteVersion); @@ -734,6 +746,14 @@ RestoreArchive(Archive *AHX) ahprintf(AH, "--\n-- PostgreSQL database dump complete\n--\n\n"); + /* + * If generating plain-text output, exit restricted mode at the very end + * of the script. This is not pro forma; in particular, pg_dumpall + * requires this when transitioning from one database to another. + */ + if (ropt->restrict_key) + ahprintf(AH, "\\unrestrict %s\n\n", ropt->restrict_key); + /* * Clean up & we're done. */ @@ -3239,11 +3259,21 @@ _reconnectToDB(ArchiveHandle *AH, const char *dbname) else { PQExpBufferData connectbuf; + RestoreOptions *ropt = AH->public.ropt; + + /* + * We must temporarily exit restricted mode for \connect, etc. + * Anything added between this line and the following \restrict must + * be careful to avoid any possible meta-command injection vectors. + */ + ahprintf(AH, "\\unrestrict %s\n", ropt->restrict_key); initPQExpBuffer(&connectbuf); appendPsqlMetaConnect(&connectbuf, dbname); - ahprintf(AH, "%s\n", connectbuf.data); + ahprintf(AH, "%s", connectbuf.data); termPQExpBuffer(&connectbuf); + + ahprintf(AH, "\\restrict %s\n\n", ropt->restrict_key); } /* diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index 96aac99a944..161dd5ca1ea 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -388,6 +388,7 @@ main(int argc, char **argv) {"on-conflict-do-nothing", no_argument, &dopt.do_nothing, 1}, {"rows-per-insert", required_argument, NULL, 10}, {"include-foreign-data", required_argument, NULL, 11}, + {"restrict-key", required_argument, NULL, 25}, {NULL, 0, NULL, 0} }; @@ -605,6 +606,10 @@ main(int argc, char **argv) optarg); break; + case 25: + dopt.restrict_key = pg_strdup(optarg); + break; + default: fprintf(stderr, _("Try \"%s --help\" for more information.\n"), progname); exit_nicely(1); @@ -673,8 +678,22 @@ main(int argc, char **argv) /* archiveFormat specific setup */ if (archiveFormat == archNull) + { plainText = 1; + /* + * If you don't provide a restrict key, one will be appointed for you. + */ + if (!dopt.restrict_key) + dopt.restrict_key = generate_restrict_key(); + if (!dopt.restrict_key) + fatal("could not generate restrict key"); + if (!valid_restrict_key(dopt.restrict_key)) + fatal("invalid restrict key"); + } + else if (dopt.restrict_key) + fatal("option --restrict-key can only be used with --format=plain"); + /* Custom and directory formats are compressed by default, others not */ if (compressLevel == -1) { @@ -949,6 +968,7 @@ main(int argc, char **argv) ropt->enable_row_security = dopt.enable_row_security; ropt->sequence_data = dopt.sequence_data; ropt->binary_upgrade = dopt.binary_upgrade; + ropt->restrict_key = dopt.restrict_key ? pg_strdup(dopt.restrict_key) : NULL; if (compressLevel == -1) ropt->compression = 0; @@ -1044,6 +1064,7 @@ help(const char *progname) printf(_(" --no-unlogged-table-data do not dump unlogged table data\n")); printf(_(" --on-conflict-do-nothing add ON CONFLICT DO NOTHING to INSERT commands\n")); printf(_(" --quote-all-identifiers quote all identifiers, even if not key words\n")); + printf(_(" --restrict-key=RESTRICT_KEY use provided string as psql \\restrict key\n")); printf(_(" --rows-per-insert=NROWS number of rows per INSERT; implies --inserts\n")); printf(_(" --section=SECTION dump named section (pre-data, data, or post-data)\n")); printf(_(" --serializable-deferrable wait until the dump can run without anomalies\n")); diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c index 7537922a8f3..94824c57e83 100644 --- a/src/bin/pg_dump/pg_dumpall.c +++ b/src/bin/pg_dump/pg_dumpall.c @@ -92,6 +92,8 @@ static char *filename = NULL; static SimpleStringList database_exclude_patterns = {NULL, NULL}; static SimpleStringList database_exclude_names = {NULL, NULL}; +static char *restrict_key; + #define exit_nicely(code) exit(code) int @@ -146,6 +148,7 @@ main(int argc, char *argv[]) {"no-unlogged-table-data", no_argument, &no_unlogged_table_data, 1}, {"on-conflict-do-nothing", no_argument, &on_conflict_do_nothing, 1}, {"rows-per-insert", required_argument, NULL, 7}, + {"restrict-key", required_argument, NULL, 9}, {NULL, 0, NULL, 0} }; @@ -334,6 +337,12 @@ main(int argc, char *argv[]) appendShellString(pgdumpopts, optarg); break; + case 9: + restrict_key = pg_strdup(optarg); + appendPQExpBufferStr(pgdumpopts, " --restrict-key "); + appendShellString(pgdumpopts, optarg); + break; + default: fprintf(stderr, _("Try \"%s --help\" for more information.\n"), progname); exit_nicely(1); @@ -432,6 +441,22 @@ main(int argc, char *argv[]) if (on_conflict_do_nothing) appendPQExpBufferStr(pgdumpopts, " --on-conflict-do-nothing"); + /* + * If you don't provide a restrict key, one will be appointed for you. + */ + if (!restrict_key) + restrict_key = generate_restrict_key(); + if (!restrict_key) + { + pg_log_error("could not generate restrict key"); + exit_nicely(1); + } + if (!valid_restrict_key(restrict_key)) + { + pg_log_error("invalid restrict key"); + exit_nicely(1); + } + /* * If there was a database specified on the command line, use that, * otherwise try to connect to database "postgres", and failing that @@ -530,6 +555,16 @@ main(int argc, char *argv[]) if (verbose) dumpTimestamp("Started on"); + /* + * Enter restricted mode to block any unexpected psql meta-commands. A + * malicious source might try to inject a variety of things via bogus + * responses to queries. While we cannot prevent such sources from + * affecting the destination at restore time, we can block psql + * meta-commands so that the client machine that runs psql with the dump + * output remains unaffected. + */ + fprintf(OPF, "\\restrict %s\n\n", restrict_key); + /* * We used to emit \connect postgres here, but that served no purpose * other than to break things for installations without a postgres @@ -589,6 +624,12 @@ main(int argc, char *argv[]) dumpTablespaces(conn); } + /* + * Exit restricted mode just before dumping the databases. pg_dump will + * handle entering restricted mode again as appropriate. + */ + fprintf(OPF, "\\unrestrict %s\n\n", restrict_key); + if (!globals_only && !roles_only && !tablespaces_only) dumpDatabases(conn); @@ -654,6 +695,7 @@ help(void) printf(_(" --no-unlogged-table-data do not dump unlogged table data\n")); printf(_(" --on-conflict-do-nothing add ON CONFLICT DO NOTHING to INSERT commands\n")); printf(_(" --quote-all-identifiers quote all identifiers, even if not key words\n")); + printf(_(" --restrict-key=RESTRICT_KEY use provided string as psql \\restrict key\n")); printf(_(" --rows-per-insert=NROWS number of rows per INSERT; implies --inserts\n")); printf(_(" --use-set-session-authorization\n" " use SET SESSION AUTHORIZATION commands instead of\n" diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c index ebdc50d921c..f883839bda8 100644 --- a/src/bin/pg_dump/pg_restore.c +++ b/src/bin/pg_dump/pg_restore.c @@ -120,6 +120,7 @@ main(int argc, char **argv) {"no-publications", no_argument, &no_publications, 1}, {"no-security-labels", no_argument, &no_security_labels, 1}, {"no-subscriptions", no_argument, &no_subscriptions, 1}, + {"restrict-key", required_argument, NULL, 6}, {NULL, 0, NULL, 0} }; @@ -280,6 +281,10 @@ main(int argc, char **argv) set_dump_section(optarg, &(opts->dumpSections)); break; + case 6: + opts->restrict_key = pg_strdup(optarg); + break; + default: fprintf(stderr, _("Try \"%s --help\" for more information.\n"), progname); exit_nicely(1); @@ -319,8 +324,33 @@ main(int argc, char **argv) progname); exit_nicely(1); } + + if (opts->restrict_key) + { + pg_log_error("options -d/--dbname and --restrict-key cannot be used together"); + exit_nicely(1); + } + opts->useDB = 1; } + else + { + /* + * If you don't provide a restrict key, one will be appointed for you. + */ + if (!opts->restrict_key) + opts->restrict_key = generate_restrict_key(); + if (!opts->restrict_key) + { + pg_log_error("could not generate restrict key"); + exit_nicely(1); + } + if (!valid_restrict_key(opts->restrict_key)) + { + pg_log_error("invalid restrict key"); + exit_nicely(1); + } + } if (opts->dataOnly && opts->schemaOnly) { @@ -500,6 +530,7 @@ usage(const char *progname) printf(_(" --no-security-labels do not restore security labels\n")); printf(_(" --no-subscriptions do not restore subscriptions\n")); printf(_(" --no-tablespaces do not restore tablespace assignments\n")); + printf(_(" --restrict-key=RESTRICT_KEY use provided string as psql \\restrict key\n")); printf(_(" --section=SECTION restore named section (pre-data, data, or post-data)\n")); printf(_(" --strict-names require table and/or schema include patterns to\n" " match at least one entity each\n")); diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index cb83714b017..bc28c03448a 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -411,6 +411,16 @@ my %full_runs = ( # This is where the actual tests are defined. my %tests = ( + 'restrict' => { + all_runs => 1, + regexp => qr/^\\restrict [a-zA-Z0-9]+$/m, + }, + + 'unrestrict' => { + all_runs => 1, + regexp => qr/^\\unrestrict [a-zA-Z0-9]+$/m, + }, + 'ALTER DEFAULT PRIVILEGES FOR ROLE regress_dump_test_role GRANT' => { create_order => 14, create_sql => 'ALTER DEFAULT PRIVILEGES @@ -2860,7 +2870,6 @@ my %tests = ( }, 'ALTER TABLE measurement PRIMARY KEY' => { - all_runs => 1, catch_all => 'CREATE ... commands', create_order => 93, create_sql => @@ -2897,7 +2906,6 @@ my %tests = ( }, 'ALTER INDEX ... ATTACH PARTITION (primary key)' => { - all_runs => 1, catch_all => 'CREATE ... commands', regexp => qr/^ \QALTER INDEX dump_test.measurement_pkey ATTACH PARTITION dump_test_second_schema.measurement_y2006m2_pkey\E @@ -3775,9 +3783,10 @@ foreach my $run (sort keys %pgdump_runs) next; } - # Run the test listed as a like, unless it is specifically noted - # as an unlike (generally due to an explicit exclusion or similar). - if ($tests{$test}->{like}->{$test_key} + # Run the test if all_runs is set or if listed as a like, unless it is + # specifically noted as an unlike (generally due to an explicit + # exclusion or similar). + if (($tests{$test}->{like}->{$test_key} || $tests{$test}->{all_runs}) && !defined($tests{$test}->{unlike}->{$test_key})) { if (!ok($output_file =~ $tests{$test}->{regexp}, diff --git a/src/bin/pg_upgrade/test.sh b/src/bin/pg_upgrade/test.sh index 9f6fb3e0185..0dbb92d0d32 100644 --- a/src/bin/pg_upgrade/test.sh +++ b/src/bin/pg_upgrade/test.sh @@ -191,6 +191,7 @@ if "$MAKE" -C "$oldsrc" installcheck-parallel; then fi pg_dumpall $extra_dump_options --no-sync \ + --restrict-key=test \ -f "$temp_root"/dump1.sql || pg_dumpall1_status=$? if [ "$newsrc" != "$oldsrc" ]; then @@ -262,6 +263,7 @@ case $testhost in esac pg_dumpall $extra_dump_options --no-sync \ + --restrict-key=test \ -f "$temp_root"/dump2.sql || pg_dumpall2_status=$? pg_ctl -m fast stop diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c index a00d1b86785..7eda42a44d2 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -114,6 +114,8 @@ static backslashResult exec_command_pset(PsqlScanState scan_state, bool active_b static backslashResult exec_command_quit(PsqlScanState scan_state, bool active_branch); static backslashResult exec_command_reset(PsqlScanState scan_state, bool active_branch, PQExpBuffer query_buf); +static backslashResult exec_command_restrict(PsqlScanState scan_state, bool active_branch, + const char *cmd); static backslashResult exec_command_s(PsqlScanState scan_state, bool active_branch); static backslashResult exec_command_set(PsqlScanState scan_state, bool active_branch); static backslashResult exec_command_setenv(PsqlScanState scan_state, bool active_branch, @@ -123,6 +125,8 @@ static backslashResult exec_command_sf_sv(PsqlScanState scan_state, bool active_ static backslashResult exec_command_t(PsqlScanState scan_state, bool active_branch); static backslashResult exec_command_T(PsqlScanState scan_state, bool active_branch); static backslashResult exec_command_timing(PsqlScanState scan_state, bool active_branch); +static backslashResult exec_command_unrestrict(PsqlScanState scan_state, bool active_branch, + const char *cmd); static backslashResult exec_command_unset(PsqlScanState scan_state, bool active_branch, const char *cmd); static backslashResult exec_command_write(PsqlScanState scan_state, bool active_branch, @@ -172,6 +176,8 @@ static char *pset_value_string(const char *param, printQueryOpt *popt); static void checkWin32Codepage(void); #endif +static bool restricted; +static char *restrict_key; /*---------- @@ -217,8 +223,19 @@ HandleSlashCmds(PsqlScanState scan_state, /* Parse off the command name */ cmd = psql_scan_slash_command(scan_state); - /* And try to execute it */ - status = exec_command(cmd, scan_state, cstack, query_buf, previous_buf); + /* + * And try to execute it. + * + * If we are in "restricted" mode, the only allowable backslash command is + * \unrestrict (to exit restricted mode). + */ + if (restricted && strcmp(cmd, "unrestrict") != 0) + { + pg_log_error("backslash commands are restricted; only \\unrestrict is allowed"); + status = PSQL_CMD_ERROR; + } + else + status = exec_command(cmd, scan_state, cstack, query_buf, previous_buf); if (status == PSQL_CMD_UNKNOWN) { @@ -375,6 +392,8 @@ exec_command(const char *cmd, status = exec_command_quit(scan_state, active_branch); else if (strcmp(cmd, "r") == 0 || strcmp(cmd, "reset") == 0) status = exec_command_reset(scan_state, active_branch, query_buf); + else if (strcmp(cmd, "restrict") == 0) + status = exec_command_restrict(scan_state, active_branch, cmd); else if (strcmp(cmd, "s") == 0) status = exec_command_s(scan_state, active_branch); else if (strcmp(cmd, "set") == 0) @@ -391,6 +410,8 @@ exec_command(const char *cmd, status = exec_command_T(scan_state, active_branch); else if (strcmp(cmd, "timing") == 0) status = exec_command_timing(scan_state, active_branch); + else if (strcmp(cmd, "unrestrict") == 0) + status = exec_command_unrestrict(scan_state, active_branch, cmd); else if (strcmp(cmd, "unset") == 0) status = exec_command_unset(scan_state, active_branch, cmd); else if (strcmp(cmd, "w") == 0 || strcmp(cmd, "write") == 0) @@ -2190,6 +2211,35 @@ exec_command_reset(PsqlScanState scan_state, bool active_branch, return PSQL_CMD_SKIP_LINE; } +/* + * \restrict -- enter "restricted mode" with the provided key + */ +static backslashResult +exec_command_restrict(PsqlScanState scan_state, bool active_branch, + const char *cmd) +{ + if (active_branch) + { + char *opt; + + Assert(!restricted); + + opt = psql_scan_slash_option(scan_state, OT_NORMAL, NULL, true); + if (opt == NULL || opt[0] == '\0') + { + pg_log_error("\\%s: missing required argument", cmd); + return PSQL_CMD_ERROR; + } + + restrict_key = pstrdup(opt); + restricted = true; + } + else + ignore_slash_options(scan_state); + + return PSQL_CMD_SKIP_LINE; +} + /* * \s -- save history in a file or show it on the screen */ @@ -2508,6 +2558,46 @@ exec_command_timing(PsqlScanState scan_state, bool active_branch) return success ? PSQL_CMD_SKIP_LINE : PSQL_CMD_ERROR; } +/* + * \unrestrict -- exit "restricted mode" if provided key matches + */ +static backslashResult +exec_command_unrestrict(PsqlScanState scan_state, bool active_branch, + const char *cmd) +{ + if (active_branch) + { + char *opt; + + opt = psql_scan_slash_option(scan_state, OT_NORMAL, NULL, true); + if (opt == NULL || opt[0] == '\0') + { + pg_log_error("\\%s: missing required argument", cmd); + return PSQL_CMD_ERROR; + } + + if (!restricted) + { + pg_log_error("\\%s: not currently in restricted mode", cmd); + return PSQL_CMD_ERROR; + } + else if (strcmp(opt, restrict_key) == 0) + { + pfree(restrict_key); + restricted = false; + } + else + { + pg_log_error("\\%s: wrong key", cmd); + return PSQL_CMD_ERROR; + } + } + else + ignore_slash_options(scan_state); + + return PSQL_CMD_SKIP_LINE; +} + /* * \unset -- unset variable */ diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c index 3cca4e93766..732e449e7be 100644 --- a/src/bin/psql/help.c +++ b/src/bin/psql/help.c @@ -182,6 +182,10 @@ slashUsage(unsigned short int pager) fprintf(output, _(" \\gset [PREFIX] execute query and store results in psql variables\n")); fprintf(output, _(" \\gx [(OPTIONS)] [FILE] as \\g, but forces expanded output mode\n")); fprintf(output, _(" \\q quit psql\n")); + fprintf(output, _(" \\restrict RESTRICT_KEY\n" + " enter restricted mode with provided key\n")); + fprintf(output, _(" \\unrestrict RESTRICT_KEY\n" + " exit restricted mode if key matches\n")); fprintf(output, _(" \\watch [SEC] execute query every SEC seconds\n")); fprintf(output, "\n"); diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c index afd9669296d..53797a8f92a 100644 --- a/src/bin/psql/tab-complete.c +++ b/src/bin/psql/tab-complete.c @@ -1527,10 +1527,10 @@ psql_completion(const char *text, int start, int end) "\\o", "\\p", "\\password", "\\prompt", "\\pset", "\\q", "\\qecho", - "\\r", + "\\r", "\\restrict", "\\s", "\\set", "\\setenv", "\\sf", "\\sv", "\\t", "\\T", "\\timing", - "\\unset", + "\\unrestrict", "\\unset", "\\x", "\\w", "\\warn", "\\watch", "\\z", diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index 9a7d43397ae..8c893b14c91 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -4433,6 +4433,7 @@ invalid command \lo \pset arg1 arg2 \q \reset + \restrict test \s arg1 \set arg1 arg2 arg3 arg4 arg5 arg6 arg7 \setenv arg1 arg2 @@ -4441,6 +4442,7 @@ invalid command \lo \t arg1 \T arg1 \timing arg1 + \unrestrict not_valid \unset arg1 \w arg1 \watch arg1 diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql index e6edf1ca0d1..3eae271ef89 100644 --- a/src/test/regress/sql/psql.sql +++ b/src/test/regress/sql/psql.sql @@ -976,6 +976,7 @@ select \if false \\ (bogus \else \\ 42 \endif \\ forty_two; \pset arg1 arg2 \q \reset + \restrict test \s arg1 \set arg1 arg2 arg3 arg4 arg5 arg6 arg7 \setenv arg1 arg2 @@ -984,6 +985,7 @@ select \if false \\ (bogus \else \\ 42 \endif \\ forty_two; \t arg1 \T arg1 \timing arg1 + \unrestrict not_valid \unset arg1 \w arg1 \watch arg1