Restrict psql meta-commands in plain-text dumps.
authorNathan Bossart <nathan@postgresql.org>
Mon, 11 Aug 2025 14:00:00 +0000 (09:00 -0500)
committerNathan Bossart <nathan@postgresql.org>
Mon, 11 Aug 2025 14:00:00 +0000 (09:00 -0500)
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

22 files changed:
doc/src/sgml/ref/pg_dump.sgml
doc/src/sgml/ref/pg_dumpall.sgml
doc/src/sgml/ref/pg_restore.sgml
doc/src/sgml/ref/pgupgrade.sgml
doc/src/sgml/ref/psql-ref.sgml
src/bin/pg_combinebackup/t/002_compare_backups.pl
src/bin/pg_dump/dumputils.c
src/bin/pg_dump/dumputils.h
src/bin/pg_dump/pg_backup.h
src/bin/pg_dump/pg_backup_archiver.c
src/bin/pg_dump/pg_dump.c
src/bin/pg_dump/pg_dumpall.c
src/bin/pg_dump/pg_restore.c
src/bin/pg_dump/t/002_pg_dump.pl
src/bin/pg_upgrade/t/002_pg_upgrade.pl
src/bin/psql/command.c
src/bin/psql/help.c
src/bin/psql/t/001_basic.pl
src/bin/psql/tab-complete.c
src/test/recovery/t/027_stream_regress.pl
src/test/regress/expected/psql.out
src/test/regress/sql/psql.sql

index cfc74ca6d694ac7ca65513335effdec950146929..5ba167b33e722ae3a955a0574834ca8260aa1fc7 100644 (file)
@@ -92,6 +92,18 @@ PostgreSQL documentation
    light of the limitations listed below.
   </para>
 
+  <warning>
+   <para>
+    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 <application>pg_restore</application>'s <option>--file</option>
+    option.  Note that the client running the dump and restore need not trust
+    the source or destination superusers.
+   </para>
+  </warning>
+
  </refsect1>
 
  <refsect1 id="pg-dump-options">
@@ -1207,6 +1219,29 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--restrict-key=<replaceable class="parameter">restrict_key</replaceable></option></term>
+      <listitem>
+       <para>
+        Use the provided string as the <application>psql</application>
+        <command>\restrict</command> key in the dump output.  This can only be
+        specified for plain-text dumps, i.e., when <option>--format</option> is
+        set to <literal>plain</literal> or the <option>--format</option> option
+        is omitted.  If no restrict key is specified,
+        <application>pg_dump</application> will generate a random one as
+        needed.  Keys may contain only alphanumeric characters.
+       </para>
+       <para>
+        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
+        <application>psql</application> with the dump output.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--rows-per-insert=<replaceable class="parameter">nrows</replaceable></option></term>
       <listitem>
index 9624144c1f4a2bfb06eda9be2c28577b67cae41a..84e38ef2115743ddc3942823c8ff4a09bf4acdf7 100644 (file)
@@ -66,6 +66,16 @@ PostgreSQL documentation
   linkend="libpq-pgpass"/> for more information.
   </para>
 
+  <warning>
+   <para>
+    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.
+   </para>
+  </warning>
+
  </refsect1>
 
  <refsect1>
@@ -555,6 +565,26 @@ exclude database <replaceable class="parameter">PATTERN</replaceable>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--restrict-key=<replaceable class="parameter">restrict_key</replaceable></option></term>
+      <listitem>
+       <para>
+        Use the provided string as the <application>psql</application>
+        <command>\restrict</command> key in the dump output.  If no restrict
+        key is specified, <application>pg_dumpall</application> will generate a
+        random one as needed.  Keys may contain only alphanumeric characters.
+       </para>
+       <para>
+        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
+        <application>psql</application> with the dump output.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--rows-per-insert=<replaceable class="parameter">nrows</replaceable></option></term>
       <listitem>
index 0f23067d784895fb53675b85a6d3235973c0fff2..8e221d89d283b36291aed39001ef278cb71c7e53 100644 (file)
@@ -68,6 +68,18 @@ PostgreSQL documentation
    <application>pg_restore</application> will not be able to load the data
    using <command>COPY</command> statements.
   </para>
+
+  <warning>
+   <para>
+    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 <application>pg_restore</application>'s <option>--file</option>
+    option.  Note that the client running the dump and restore need not trust
+    the source or destination superusers.
+   </para>
+  </warning>
  </refsect1>
 
  <refsect1 id="app-pgrestore-options">
@@ -755,6 +767,28 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--restrict-key=<replaceable class="parameter">restrict_key</replaceable></option></term>
+      <listitem>
+       <para>
+        Use the provided string as the <application>psql</application>
+        <command>\restrict</command> key in the dump output.  This can only be
+        specified for SQL script output, i.e., when the <option>--file</option>
+        option is used.  If no restrict key is specified,
+        <application>pg_restore</application> will generate a random one as
+        needed.  Keys may contain only alphanumeric characters.
+       </para>
+       <para>
+        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
+        <application>psql</application> with the dump output.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
        <term><option>--section=<replaceable class="parameter">sectionname</replaceable></option></term>
        <listitem>
index 9877f2f01c69139887b7cc32eba5c6abb9ca9b64..cdc37e91abe1107e442bd69bcf34171c68cafee7 100644 (file)
@@ -70,6 +70,14 @@ PostgreSQL documentation
    pg_upgrade supports upgrades from 9.2.X and later to the current
    major release of <productname>PostgreSQL</productname>, including snapshot and beta releases.
   </para>
+
+  <warning>
+   <para>
+    Upgrading a cluster causes the destination to execute arbitrary code of the
+    source superusers' choice.  Ensure that the source superusers are trusted
+    before upgrading.
+   </para>
+  </warning>
  </refsect1>
 
  <refsect1>
index 830306ea1e202333547c4613abd29f2a8f6b75b4..475b84ef54a661f0a5687ee07e34fb1d5e89e392 100644 (file)
@@ -3340,6 +3340,24 @@ lo_import 152801
       </varlistentry>
 
 
+      <varlistentry id="app-psql-meta-command-restrict">
+        <term><literal>\restrict <replaceable class="parameter">restrict_key</replaceable></literal></term>
+        <listitem>
+        <para>
+        Enter "restricted" mode with the provided key.  In this mode, the only
+        allowed meta-command is <command>\unrestrict</command>, to exit
+        restricted mode.  The key may contain only alphanumeric characters.
+        </para>
+        <para>
+        This command is primarily intended for use in plain-text dumps
+        generated by <application>pg_dump</application>,
+        <application>pg_dumpall</application>, and
+        <application>pg_restore</application>, but it may be useful elsewhere.
+        </para>
+        </listitem>
+      </varlistentry>
+
+
       <varlistentry id="app-psql-meta-command-s">
         <term><literal>\s [ <replaceable class="parameter">filename</replaceable> ]</literal></term>
         <listitem>
@@ -3514,6 +3532,24 @@ testdb=&gt; <userinput>\setenv LESS -imx4F</userinput>
       </varlistentry>
 
 
+      <varlistentry id="app-psql-meta-command-unrestrict">
+        <term><literal>\unrestrict <replaceable class="parameter">restrict_key</replaceable></literal></term>
+        <listitem>
+        <para>
+        Exit "restricted" mode (i.e., where all other meta-commands are
+        blocked), provided the specified key matches the one given to
+        <command>\restrict</command> when restricted mode was entered.
+        </para>
+        <para>
+        This command is primarily intended for use in plain-text dumps
+        generated by <application>pg_dump</application>,
+        <application>pg_dumpall</application>, and
+        <application>pg_restore</application>, but it may be useful elsewhere.
+        </para>
+        </listitem>
+      </varlistentry>
+
+
       <varlistentry id="app-psql-meta-command-unset">
         <term><literal>\unset <replaceable class="parameter">name</replaceable></literal></term>
 
index cfdd25471cb5f903ad9a0bb1ebeb579852d00ea5..cfb5ce83a9221201e81005247b137f76acd52814 100644 (file)
@@ -171,6 +171,7 @@ $pitr1->command_ok(
    [
        'pg_dumpall', '-f',
        $dump1, '--no-sync',
+       '--restrict-key=test',
        '--no-unlogged-table-data', '-d',
        $pitr1->connstr('postgres'),
    ],
@@ -179,6 +180,7 @@ $pitr2->command_ok(
    [
        'pg_dumpall', '-f',
        $dump2, '--no-sync',
+       '--restrict-key=test',
        '--no-unlogged-table-data', '-d',
        $pitr2->connstr('postgres'),
    ],
index 9e5748311e0472cb7f68f40c5c4db05a99f4fc76..47522d05429bd7735b809b367b806c758d295e52 100644 (file)
@@ -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,
@@ -920,3 +921,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);
+}
index d1248d9061ba1342aedb8d92cd0481b9edfcc60e..f685c616c6461073e3277db1af925625dc4f373d 100644 (file)
@@ -64,4 +64,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 */
index fbf5f1c515e5e1436f7fc6d7dbf041bfc8fb3d26..609635ccbcb93dfd061e56a4e9696aaa1536ceeb 100644 (file)
@@ -157,6 +157,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
@@ -203,6 +205,8 @@ typedef struct _dumpOptions
 
    int         sequence_data;  /* dump sequence data even in schema-only mode */
    int         do_nothing;
+
+   char       *restrict_key;
 } DumpOptions;
 
 /*
index 9d0b5d0bd7f87010c12599e6ea330d41c271a5f6..4717e6f6f83258b58ff374a1073cad08c2ba4ef1 100644 (file)
@@ -187,6 +187,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;
 }
@@ -451,6 +452,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);
@@ -791,6 +803,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.
     */
@@ -3349,11 +3369,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);
    }
 
    /*
index 201b17697a410ffcfc7f7130691acc812673da57..13139c9f0781f556d078016dd07c18e441b639e9 100644 (file)
@@ -450,6 +450,7 @@ main(int argc, char **argv)
        {"sync-method", required_argument, NULL, 15},
        {"filter", required_argument, NULL, 16},
        {"exclude-extension", required_argument, NULL, 17},
+       {"restrict-key", required_argument, NULL, 25},
 
        {NULL, 0, NULL, 0}
    };
@@ -690,6 +691,10 @@ main(int argc, char **argv)
                                          optarg);
                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);
@@ -752,8 +757,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
@@ -1053,6 +1072,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;
 
@@ -1160,6 +1180,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"));
index 1f98c89c34a70a951be7e72a64210ab70e284f44..be18bf0146b5911fe896013a06be1db81004150e 100644 (file)
@@ -121,6 +121,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
@@ -178,6 +180,7 @@ main(int argc, char *argv[])
        {"on-conflict-do-nothing", no_argument, &on_conflict_do_nothing, 1},
        {"rows-per-insert", required_argument, NULL, 7},
        {"filter", required_argument, NULL, 8},
+       {"restrict-key", required_argument, NULL, 9},
 
        {NULL, 0, NULL, 0}
    };
@@ -365,6 +368,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);
@@ -460,6 +469,16 @@ 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_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
@@ -547,6 +566,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
@@ -607,6 +636,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);
 
@@ -675,6 +710,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"
index df119591ccaa0f207d51438cdf98c8f9b18c35b8..fc6fa923231f27af7793b95d755b42644db104ac 100644 (file)
@@ -127,6 +127,7 @@ main(int argc, char **argv)
        {"no-security-labels", no_argument, &no_security_labels, 1},
        {"no-subscriptions", no_argument, &no_subscriptions, 1},
        {"filter", required_argument, NULL, 4},
+       {"restrict-key", required_argument, NULL, 6},
 
        {NULL, 0, NULL, 0}
    };
@@ -302,6 +303,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);
@@ -337,8 +342,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");
+   }
 
    if (opts->dataOnly && opts->schemaOnly)
        pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
@@ -493,6 +514,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(_("  --strict-names               require table and/or schema include patterns to\n"
             "                               match at least one entity each\n"));
index 5e256b52d24e672375f8e74193db8bda4cc0245f..8d5b2e1dc45e0324e5aff89772ea7e58e7007954 100644 (file)
@@ -718,6 +718,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
@@ -3875,7 +3885,6 @@ my %tests = (
    },
 
    'ALTER TABLE measurement PRIMARY KEY' => {
-       all_runs => 1,
        catch_all => 'CREATE ... commands',
        create_order => 93,
        create_sql =>
@@ -3927,7 +3936,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
@@ -4952,9 +4960,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\"";
        }
@@ -4990,9 +4999,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},
index 78bd776f5be2f19d56124e22633e5c119827c993..649be522dc0a15c365ef1f392ee9d42a7bb87497 100644 (file)
@@ -315,6 +315,7 @@ if (defined($ENV{oldinstall}))
 # that we need to use pg_dumpall from the new node here.
 my @dump_command = (
    'pg_dumpall', '--no-sync', '-d', $oldnode->connstr('postgres'),
+   '--restrict-key=test',
    '-f', $dump1_file);
 # --extra-float-digits is needed when upgrading from a version older than 11.
 push(@dump_command, '--extra-float-digits', '0')
@@ -500,6 +501,7 @@ is( $result,
 # Second dump from the upgraded instance.
 @dump_command = (
    'pg_dumpall', '--no-sync', '-d', $newnode->connstr('postgres'),
+   '--restrict-key=test',
    '-f', $dump2_file);
 # --extra-float-digits is needed when upgrading from a version older than 11.
 push(@dump_command, '--extra-float-digits', '0')
index f3f8fd0765ac713cb6d296dc041bd380775d8f38..877ed0796d648fcf0af8110e5f825becf7ba6655 100644 (file)
@@ -123,6 +123,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,
@@ -132,6 +134,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,
@@ -182,6 +186,8 @@ static char *pset_value_string(const char *param, printQueryOpt *popt);
 static void checkWin32Codepage(void);
 #endif
 
+static bool restricted;
+static char *restrict_key;
 
 
 /*----------
@@ -227,8 +233,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)
    {
@@ -389,6 +406,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)
@@ -405,6 +424,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)
@@ -2337,6 +2358,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
  */
@@ -2624,6 +2674,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
  */
index b880fa2c1dd1e7c65d3ea721856dfe58d2e1d202..eb3b80f4520909dee9bc3380de5fe199e62d9f17 100644 (file)
@@ -175,6 +175,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");
index bd4fdd2030a373ef9ea2c1efd81f9dec6bfb27b5..96c0a064ae91374896722374e4f183294832f17d 100644 (file)
@@ -443,4 +443,11 @@ psql_like($node, "copy (values ('foo'),('bar')) to stdout \\g | $pipe_cmd",
 my $c4 = slurp_file($g_file);
 like($c4, qr/foo.*bar/s);
 
+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();
index 6c62c07ce821918e290e2b3208f8866b012ca8d8..d4c5f6c3798bed68663e3843bc33f4753214e260 100644 (file)
@@ -1746,10 +1746,10 @@ psql_completion(const char *text, int start, int end)
        "\\out",
        "\\password", "\\print", "\\prompt", "\\pset",
        "\\qecho", "\\quit",
-       "\\reset",
+       "\\reset", "\\restrict",
        "\\s", "\\set", "\\setenv", "\\sf", "\\sv",
        "\\t", "\\T", "\\timing",
-       "\\unset",
+       "\\unrestrict", "\\unset",
        "\\x",
        "\\warn", "\\watch", "\\write",
        "\\z",
index d1ae32d97d603ea7f899b00a44a241bb8c652526..7bd9054a618c0c65a5ca1420b68426a3e4dd9c48 100644 (file)
@@ -106,6 +106,7 @@ $node_primary->wait_for_replay_catchup($node_standby_1);
 command_ok(
    [
        'pg_dumpall', '-f', $outputdir . '/primary.dump',
+       '--restrict-key=test',
        '--no-sync', '-p', $node_primary->port,
        '--no-unlogged-table-data'    # if unlogged, standby has schema only
    ],
@@ -113,6 +114,7 @@ command_ok(
 command_ok(
    [
        'pg_dumpall', '-f', $outputdir . '/standby.dump',
+       '--restrict-key=test',
        '--no-sync', '-p', $node_standby_1->port
    ],
    'dump standby server');
@@ -131,6 +133,7 @@ command_ok(
        ('--schema', 'pg_catalog'),
        ('-f', $outputdir . '/catalogs_primary.dump'),
        '--no-sync',
+       '--restrict-key=test',
        ('-p', $node_primary->port),
        '--no-unlogged-table-data',
        'regression'
@@ -142,6 +145,7 @@ command_ok(
        ('--schema', 'pg_catalog'),
        ('-f', $outputdir . '/catalogs_standby.dump'),
        '--no-sync',
+       '--restrict-key=test',
        ('-p', $node_standby_1->port),
        'regression'
    ],
index 3bbe4c5f974d6fd70888e307ccbb63f2cbb2a144..7e39eef6ec0e539b6dc7a23c5e764a646f02d1d9 100644 (file)
@@ -4543,6 +4543,7 @@ invalid command \lo
    \pset arg1 arg2
    \q
    \reset
+   \restrict test
    \s arg1
    \set arg1 arg2 arg3 arg4 arg5 arg6 arg7
    \setenv arg1 arg2
@@ -4551,6 +4552,7 @@ invalid command \lo
    \t arg1
    \T arg1
    \timing arg1
+   \unrestrict not_valid
    \unset arg1
    \w arg1
    \watch arg1 arg2
index 3b3c6f6e2944d17bff47db81b5000e7153935a2d..f8ad7af5a3a84ad1c4195645ed4f8a99191f369c 100644 (file)
@@ -1025,6 +1025,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
@@ -1033,6 +1034,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