diff options
author | Nathan Bossart <nathan@postgresql.org> | 2025-08-11 09:00:00 -0500 |
---|---|---|
committer | Nathan Bossart <nathan@postgresql.org> | 2025-08-11 09:00:00 -0500 |
commit | 71ea0d6795438f95f4ee6e35867058c44b270d51 (patch) | |
tree | beaa29a50fdee7dec68151f94179549ccee58b7b /src | |
parent | 70693c645f6e490b9ed450e8611e94ab7af3aad2 (diff) |
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 <litezeraw@gmail.com>
Reported-by: RyotaK <ryotak.mail@gmail.com>
Suggested-by: Tom Lane <tgl@sss.pgh.pa.us>
Reviewed-by: Noah Misch <noah@leadboat.com>
Reviewed-by: Michael Paquier <michael@paquier.xyz>
Reviewed-by: Peter Eisentraut <peter@eisentraut.org>
Security: CVE-2025-8714
Backpatch-through: 13
Diffstat (limited to 'src')
-rw-r--r-- | src/bin/pg_combinebackup/t/002_compare_backups.pl | 2 | ||||
-rw-r--r-- | src/bin/pg_dump/dumputils.c | 38 | ||||
-rw-r--r-- | src/bin/pg_dump/dumputils.h | 3 | ||||
-rw-r--r-- | src/bin/pg_dump/pg_backup.h | 4 | ||||
-rw-r--r-- | src/bin/pg_dump/pg_backup_archiver.c | 32 | ||||
-rw-r--r-- | src/bin/pg_dump/pg_dump.c | 21 | ||||
-rw-r--r-- | src/bin/pg_dump/pg_dumpall.c | 36 | ||||
-rw-r--r-- | src/bin/pg_dump/pg_restore.c | 23 | ||||
-rw-r--r-- | src/bin/pg_dump/t/002_pg_dump.pl | 26 | ||||
-rw-r--r-- | src/bin/pg_upgrade/t/002_pg_upgrade.pl | 3 | ||||
-rw-r--r-- | src/bin/psql/command.c | 94 | ||||
-rw-r--r-- | src/bin/psql/help.c | 4 | ||||
-rw-r--r-- | src/bin/psql/t/001_basic.pl | 7 | ||||
-rw-r--r-- | src/bin/psql/tab-complete.in.c | 4 | ||||
-rw-r--r-- | src/test/recovery/t/027_stream_regress.pl | 4 | ||||
-rw-r--r-- | src/test/regress/expected/psql.out | 2 | ||||
-rw-r--r-- | src/test/regress/sql/psql.sql | 2 |
17 files changed, 292 insertions, 13 deletions
diff --git a/src/bin/pg_combinebackup/t/002_compare_backups.pl b/src/bin/pg_combinebackup/t/002_compare_backups.pl index 2c7ca89b92f..9c7fe56cb7c 100644 --- a/src/bin/pg_combinebackup/t/002_compare_backups.pl +++ b/src/bin/pg_combinebackup/t/002_compare_backups.pl @@ -174,6 +174,7 @@ my $dump2 = $backupdir . '/pitr2.dump'; $pitr1->command_ok( [ 'pg_dumpall', + '--restrict-key=test', '--no-sync', '--no-unlogged-table-data', '--file' => $dump1, @@ -183,6 +184,7 @@ $pitr1->command_ok( $pitr2->command_ok( [ 'pg_dumpall', + '--restrict-key=test', '--no-sync', '--no-unlogged-table-data', '--file' => $dump2, diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c index 4c02a9863c4..8945bdd42c5 100644 --- a/src/bin/pg_dump/dumputils.c +++ b/src/bin/pg_dump/dumputils.c @@ -21,6 +21,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, @@ -957,3 +958,40 @@ create_or_open_dir(const char *dirname) pg_fatal("directory \"%s\" is not empty", dirname); } } + +/* + * 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 71dd3f12a8d..10f6e27c0a0 100644 --- a/src/bin/pg_dump/dumputils.h +++ b/src/bin/pg_dump/dumputils.h @@ -65,4 +65,7 @@ extern void makeAlterConfigCommand(PGconn *conn, const char *configitem, PQExpBuffer buf); extern void create_or_open_dir(const char *dirname); +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 4ebef1e8644..d9041dad720 100644 --- a/src/bin/pg_dump/pg_backup.h +++ b/src/bin/pg_dump/pg_backup.h @@ -163,6 +163,8 @@ typedef struct _restoreOptions bool dumpSchema; bool dumpData; bool dumpStatistics; + + char *restrict_key; } RestoreOptions; typedef struct _dumpOptions @@ -213,6 +215,8 @@ typedef struct _dumpOptions bool dumpSchema; bool dumpData; bool dumpStatistics; + + 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 248e4151552..3c3acbaccdb 100644 --- a/src/bin/pg_dump/pg_backup_archiver.c +++ b/src/bin/pg_dump/pg_backup_archiver.c @@ -197,6 +197,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; } @@ -461,6 +462,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); @@ -802,6 +814,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. */ AH->stage = STAGE_FINALIZING; @@ -3452,11 +3472,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 66380a6d301..fc7a6639163 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -537,6 +537,7 @@ main(int argc, char **argv) {"filter", required_argument, NULL, 16}, {"exclude-extension", required_argument, NULL, 17}, {"sequence-data", no_argument, &dopt.sequence_data, 1}, + {"restrict-key", required_argument, NULL, 25}, {NULL, 0, NULL, 0} }; @@ -797,6 +798,10 @@ main(int argc, char **argv) with_statistics = true; break; + case 25: + dopt.restrict_key = pg_strdup(optarg); + break; + default: /* getopt_long already emitted a complaint */ pg_log_error_hint("Try \"%s --help\" for more information.", progname); @@ -889,8 +894,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) + pg_fatal("could not generate restrict key"); + if (!valid_restrict_key(dopt.restrict_key)) + pg_fatal("invalid restrict key"); + } + else if (dopt.restrict_key) + pg_fatal("option --restrict-key can only be used with --format=plain"); + /* * Custom and directory formats are compressed by default with gzip when * available, not the others. If gzip is not available, no compression is @@ -1229,6 +1248,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; ropt->compression_spec = compression_spec; @@ -1340,6 +1360,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(_(" --sequence-data include sequence data in dump\n")); diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c index 3e7f930c940..bb451c1bae1 100644 --- a/src/bin/pg_dump/pg_dumpall.c +++ b/src/bin/pg_dump/pg_dumpall.c @@ -122,6 +122,8 @@ static char *filename = NULL; static SimpleStringList database_exclude_patterns = {NULL, NULL}; static SimpleStringList database_exclude_names = {NULL, NULL}; +static char *restrict_key; + int main(int argc, char *argv[]) { @@ -184,6 +186,7 @@ main(int argc, char *argv[]) {"statistics-only", no_argument, &statistics_only, 1}, {"filter", required_argument, NULL, 8}, {"sequence-data", no_argument, &sequence_data, 1}, + {"restrict-key", required_argument, NULL, 9}, {NULL, 0, NULL, 0} }; @@ -371,6 +374,12 @@ main(int argc, char *argv[]) read_dumpall_filters(optarg, &database_exclude_patterns); break; + case 9: + restrict_key = pg_strdup(optarg); + appendPQExpBufferStr(pgdumpopts, " --restrict-key "); + appendShellString(pgdumpopts, optarg); + break; + default: /* getopt_long already emitted a complaint */ pg_log_error_hint("Try \"%s --help\" for more information.", progname); @@ -481,6 +490,16 @@ main(int argc, char *argv[]) appendPQExpBufferStr(pgdumpopts, " --sequence-data"); /* + * 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_fatal("could not generate restrict key"); + if (!valid_restrict_key(restrict_key)) + pg_fatal("invalid restrict key"); + + /* * If there was a database specified on the command line, use that, * otherwise try to connect to database "postgres", and failing that * "template1". @@ -571,6 +590,16 @@ main(int argc, char *argv[]) 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 * database. Everything we're restoring here is a global, so whichever @@ -630,6 +659,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); @@ -702,6 +737,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(_(" --sequence-data include sequence data in dump\n")); printf(_(" --statistics dump the statistics\n")); diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c index 6c129278bc5..c9776306c5c 100644 --- a/src/bin/pg_dump/pg_restore.c +++ b/src/bin/pg_dump/pg_restore.c @@ -45,6 +45,7 @@ #include <termios.h> #endif +#include "dumputils.h" #include "fe_utils/option_utils.h" #include "filter.h" #include "getopt_long.h" @@ -140,6 +141,7 @@ main(int argc, char **argv) {"statistics", no_argument, &with_statistics, 1}, {"statistics-only", no_argument, &statistics_only, 1}, {"filter", required_argument, NULL, 4}, + {"restrict-key", required_argument, NULL, 6}, {NULL, 0, NULL, 0} }; @@ -315,6 +317,10 @@ main(int argc, char **argv) opts->exit_on_error = true; break; + case 6: + opts->restrict_key = pg_strdup(optarg); + break; + default: /* getopt_long already emitted a complaint */ pg_log_error_hint("Try \"%s --help\" for more information.", progname); @@ -350,8 +356,24 @@ main(int argc, char **argv) pg_log_error_hint("Try \"%s --help\" for more information.", progname); exit_nicely(1); } + + if (opts->restrict_key) + pg_fatal("options -d/--dbname and --restrict-key cannot be used together"); + 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_fatal("could not generate restrict key"); + if (!valid_restrict_key(opts->restrict_key)) + pg_fatal("invalid restrict key"); + } /* reject conflicting "-only" options */ if (data_only && schema_only) @@ -546,6 +568,7 @@ usage(const char *progname) printf(_(" --no-subscriptions do not restore subscriptions\n")); printf(_(" --no-table-access-method do not restore table access methods\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(_(" --statistics restore the statistics\n")); printf(_(" --statistics-only restore only the statistics, not schema or data\n")); diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index e3257231a9e..e7a2d64f741 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -881,6 +881,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 @@ -4239,7 +4249,6 @@ my %tests = ( }, 'ALTER TABLE measurement PRIMARY KEY' => { - all_runs => 1, catch_all => 'CREATE ... commands', create_order => 93, create_sql => @@ -4291,7 +4300,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 @@ -5424,9 +5432,10 @@ foreach my $run (sort keys %pgdump_runs) # Check for proper test definitions # - # There should be a "like" list, even if it is empty. (This - # makes the test more self-documenting.) - if (!defined($tests{$test}->{like})) + # Either "all_runs" should be set or there should be a "like" list, + # even if it is empty. (This makes the test more self-documenting.) + if (!defined($tests{$test}->{all_runs}) + && !defined($tests{$test}->{like})) { die "missing \"like\" in test \"$test\""; } @@ -5462,9 +5471,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/t/002_pg_upgrade.pl b/src/bin/pg_upgrade/t/002_pg_upgrade.pl index 0b15e38297e..4b5e895809b 100644 --- a/src/bin/pg_upgrade/t/002_pg_upgrade.pl +++ b/src/bin/pg_upgrade/t/002_pg_upgrade.pl @@ -86,6 +86,7 @@ sub get_dump_for_comparison $node->run_log( [ 'pg_dump', '--no-sync', + '--restrict-key=test', '-d' => $node->connstr($db), '-f' => $dumpfile ]); @@ -427,6 +428,7 @@ SKIP: # that we need to use pg_dumpall from the new node here. my @dump_command = ( 'pg_dumpall', '--no-sync', + '--restrict-key=test', '--dbname' => $oldnode->connstr('postgres'), '--file' => $dump1_file); # --extra-float-digits is needed when upgrading from a version older than 11. @@ -624,6 +626,7 @@ is( $result, # Second dump from the upgraded instance. @dump_command = ( 'pg_dumpall', '--no-sync', + '--restrict-key=test', '--dbname' => $newnode->connstr('postgres'), '--file' => $dump2_file); # --extra-float-digits is needed when upgrading from a version older than 11. diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c index 0e00d73487c..cc602087db2 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -130,6 +130,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_sendpipeline(PsqlScanState scan_state, bool active_branch); static backslashResult exec_command_set(PsqlScanState scan_state, bool active_branch); @@ -142,6 +144,8 @@ static backslashResult exec_command_syncpipeline(PsqlScanState scan_state, bool 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, @@ -192,6 +196,8 @@ static char *pset_value_string(const char *param, printQueryOpt *popt); static void checkWin32Codepage(void); #endif +static bool restricted; +static char *restrict_key; /*---------- @@ -237,8 +243,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) { @@ -416,6 +433,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, "sendpipeline") == 0) @@ -438,6 +457,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) @@ -2755,6 +2776,35 @@ exec_command_reset(PsqlScanState scan_state, bool active_branch, } /* + * \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 */ static backslashResult @@ -3136,6 +3186,46 @@ exec_command_timing(PsqlScanState scan_state, bool active_branch) } /* + * \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 */ static backslashResult diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c index 8c62729a0d1..ed00c36695e 100644 --- a/src/bin/psql/help.c +++ b/src/bin/psql/help.c @@ -171,6 +171,10 @@ slashUsage(unsigned short int pager) HELP0(" \\gset [PREFIX] execute query and store result in psql variables\n"); HELP0(" \\gx [(OPTIONS)] [FILE] as \\g, but forces expanded output mode\n"); HELP0(" \\q quit psql\n"); + HELP0(" \\restrict RESTRICT_KEY\n" + " enter restricted mode with provided key\n"); + HELP0(" \\unrestrict RESTRICT_KEY\n" + " exit restricted mode if key matches\n"); HELP0(" \\watch [[i=]SEC] [c=N] [m=MIN]\n" " execute query every SEC seconds, up to N times,\n" " stop if less than MIN rows are returned\n"); diff --git a/src/bin/psql/t/001_basic.pl b/src/bin/psql/t/001_basic.pl index f42c3961e09..cf07a9dbd5e 100644 --- a/src/bin/psql/t/001_basic.pl +++ b/src/bin/psql/t/001_basic.pl @@ -530,4 +530,11 @@ psql_fails_like( qr/COPY in a pipeline is not supported, aborting connection/, '\copy to in pipeline: fails'); +psql_fails_like( + $node, + qq{\\restrict test +\\! should_fail}, + qr/backslash commands are restricted; only \\unrestrict is allowed/, + 'meta-command in restrict mode fails'); + done_testing(); diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 1f2ca946fc5..8b10f2313f3 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -1918,11 +1918,11 @@ psql_completion(const char *text, int start, int end) "\\out", "\\parse", "\\password", "\\print", "\\prompt", "\\pset", "\\qecho", "\\quit", - "\\reset", + "\\reset", "\\restrict", "\\s", "\\sendpipeline", "\\set", "\\setenv", "\\sf", "\\startpipeline", "\\sv", "\\syncpipeline", "\\t", "\\T", "\\timing", - "\\unset", + "\\unrestrict", "\\unset", "\\x", "\\warn", "\\watch", "\\write", "\\z", diff --git a/src/test/recovery/t/027_stream_regress.pl b/src/test/recovery/t/027_stream_regress.pl index 5d2c06ba06e..12d8852fb4b 100644 --- a/src/test/recovery/t/027_stream_regress.pl +++ b/src/test/recovery/t/027_stream_regress.pl @@ -117,6 +117,7 @@ command_ok( 'pg_dumpall', '--file' => $outputdir . '/primary.dump', '--no-sync', '--no-statistics', + '--restrict-key=test', '--port' => $node_primary->port, '--no-unlogged-table-data', # if unlogged, standby has schema only ], @@ -126,6 +127,7 @@ command_ok( 'pg_dumpall', '--file' => $outputdir . '/standby.dump', '--no-sync', '--no-statistics', + '--restrict-key=test', '--port' => $node_standby_1->port, ], 'dump standby server'); @@ -145,6 +147,7 @@ command_ok( '--schema' => 'pg_catalog', '--file' => $outputdir . '/catalogs_primary.dump', '--no-sync', + '--restrict-key=test', '--port', $node_primary->port, '--no-unlogged-table-data', 'regression', @@ -156,6 +159,7 @@ command_ok( '--schema' => 'pg_catalog', '--file' => $outputdir . '/catalogs_standby.dump', '--no-sync', + '--restrict-key=test', '--port' => $node_standby_1->port, 'regression', ], diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index 236eba2540e..a79325e8a2f 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -4705,6 +4705,7 @@ invalid command \lo \pset arg1 arg2 \q \reset + \restrict test \s arg1 \sendpipeline \set arg1 arg2 arg3 arg4 arg5 arg6 arg7 @@ -4716,6 +4717,7 @@ invalid command \lo \t arg1 \T arg1 \timing arg1 + \unrestrict not_valid \unset arg1 \w arg1 \watch arg1 arg2 diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql index e2e31245439..f064e4f5456 100644 --- a/src/test/regress/sql/psql.sql +++ b/src/test/regress/sql/psql.sql @@ -1073,6 +1073,7 @@ select \if false \\ (bogus \else \\ 42 \endif \\ forty_two; \pset arg1 arg2 \q \reset + \restrict test \s arg1 \sendpipeline \set arg1 arg2 arg3 arg4 arg5 arg6 arg7 @@ -1084,6 +1085,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 arg2 |