Empty search_path in Autovacuum and non-psql/pgbench clients.
authorNoah Misch
Mon, 26 Feb 2018 15:39:44 +0000 (07:39 -0800)
committerNoah Misch
Mon, 26 Feb 2018 15:39:48 +0000 (07:39 -0800)
This makes the client programs behave as documented regardless of the
connect-time search_path and regardless of user-created objects.  Today,
a malicious user with CREATE permission on a search_path schema can take
control of certain of these clients' queries and invoke arbitrary SQL
functions under the client identity, often a superuser.  This is
exploitable in the default configuration, where all users have CREATE
privilege on schema "public".

This changes behavior of user-defined code stored in the database, like
pg_index.indexprs and pg_extension_config_dump().  If they reach code
bearing unqualified names, "does not exist" or "no schema has been
selected to create in" errors might appear.  Users may fix such errors
by schema-qualifying affected names.  After upgrading, consider watching
server logs for these errors.

The --table arguments of src/bin/scripts clients have been lax; for
example, "vacuumdb -Zt pg_am\;CHECKPOINT" performed a checkpoint.  That
now fails, but for now, "vacuumdb -Zt 'pg_am(amname);CHECKPOINT'" still
performs a checkpoint.

Back-patch to 9.3 (all supported versions).

Reviewed by Tom Lane, though this fix strategy was not his first choice.
Reported by Arseniy Sharoglazov.

Security: CVE-2018-1058

27 files changed:
contrib/oid2name/oid2name.c
contrib/pg_upgrade/server.c
contrib/vacuumlo/vacuumlo.c
src/backend/postmaster/autovacuum.c
src/bin/pg_basebackup/streamutil.c
src/bin/pg_dump/dumputils.c
src/bin/pg_dump/pg_backup_db.c
src/bin/pg_dump/pg_dump.c
src/bin/pg_dump/pg_dumpall.c
src/bin/scripts/Makefile
src/bin/scripts/clusterdb.c
src/bin/scripts/common.c
src/bin/scripts/common.h
src/bin/scripts/createdb.c
src/bin/scripts/createlang.c
src/bin/scripts/createuser.c
src/bin/scripts/dropdb.c
src/bin/scripts/droplang.c
src/bin/scripts/dropuser.c
src/bin/scripts/reindexdb.c
src/bin/scripts/t/010_clusterdb.pl
src/bin/scripts/t/090_reindexdb.pl
src/bin/scripts/t/100_vacuumdb.pl
src/bin/scripts/vacuumdb.c
src/include/Makefile
src/include/fe_utils/connect.h [new file with mode: 0644]
src/tools/findoidjoins/findoidjoins.c

index e5eeec21c153f6138d58ae77cb2d9cb824f6bba2..91da40352b4bacd745bdb196ee6edc3bb174b4df 100644 (file)
@@ -9,6 +9,7 @@
  */
 #include "postgres_fe.h"
 
+#include "fe_utils/connect.h"
 #include "libpq-fe.h"
 #include "pg_getopt.h"
 
@@ -263,6 +264,7 @@ sql_conn(struct options * my_opts)
    PGconn     *conn;
    char       *password = NULL;
    bool        new_pass;
+   PGresult   *res;
 
    /*
     * Start the connection.  Loop until we have a password if requested by
@@ -322,6 +324,17 @@ sql_conn(struct options * my_opts)
        exit(1);
    }
 
+   res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
+   if (PQresultStatus(res) != PGRES_TUPLES_OK)
+   {
+       fprintf(stderr, "oid2name: could not clear search_path: %s\n",
+               PQerrorMessage(conn));
+       PQclear(res);
+       PQfinish(conn);
+       exit(-1);
+   }
+   PQclear(res);
+
    /* return the conn if good */
    return conn;
 }
index e03e526987b2035ba5f422bae648d5e2bd5904d1..908efe66b8d4ec2fc1b21605c5f07485e6f74447 100644 (file)
@@ -9,6 +9,7 @@
 
 #include "postgres_fe.h"
 
+#include "fe_utils/connect.h"
 #include "pg_upgrade.h"
 
 
@@ -39,6 +40,8 @@ connectToServer(ClusterInfo *cluster, const char *db_name)
        exit(1);
    }
 
+   PQclear(executeQueryOrDie(conn, ALWAYS_SECURE_SEARCH_PATH_SQL));
+
    return conn;
 }
 
index c2e5bad438b6184159db5162d73cfd8773a423ea..10daa85445c24ff1a90e05eb8a01420a8e037a68 100644 (file)
@@ -21,6 +21,7 @@
 #include 
 #endif
 
+#include "fe_utils/connect.h"
 #include "libpq-fe.h"
 #include "pg_getopt.h"
 
@@ -135,11 +136,8 @@ vacuumlo(const char *database, const struct _param * param)
            fprintf(stdout, "Test run: no large objects will be removed!\n");
    }
 
-   /*
-    * Don't get fooled by any non-system catalogs
-    */
-   res = PQexec(conn, "SET search_path = pg_catalog");
-   if (PQresultStatus(res) != PGRES_COMMAND_OK)
+   res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
+   if (PQresultStatus(res) != PGRES_TUPLES_OK)
    {
        fprintf(stderr, "Failed to set search_path:\n");
        fprintf(stderr, "%s", PQerrorMessage(conn));
index 48da5fa4565c82c3b2d88bd3ea53a12fb1caef64..f07f584f9364669c847cd0a2dc6c2027713c3393 100644 (file)
@@ -556,6 +556,12 @@ AutoVacLauncherMain(int argc, char *argv[])
    /* must unblock signals before calling rebuild_database_list */
    PG_SETMASK(&UnBlockSig);
 
+   /*
+    * Set always-secure search path.  Launcher doesn't connect to a database,
+    * so this has no effect.
+    */
+   SetConfigOption("search_path", "", PGC_SUSET, PGC_S_OVERRIDE);
+
    /*
     * Force zero_damaged_pages OFF in the autovac process, even if it is set
     * in postgresql.conf.  We don't really want such a dangerous option being
@@ -1598,6 +1604,14 @@ AutoVacWorkerMain(int argc, char *argv[])
 
    PG_SETMASK(&UnBlockSig);
 
+   /*
+    * Set always-secure search path, so malicious users can't redirect user
+    * code (e.g. pg_index.indexprs).  (That code runs in a
+    * SECURITY_RESTRICTED_OPERATION sandbox, so malicious users could not
+    * take control of the entire autovacuum worker in any case.)
+    */
+   SetConfigOption("search_path", "", PGC_SUSET, PGC_S_OVERRIDE);
+
    /*
     * Force zero_damaged_pages OFF in the autovac process, even if it is set
     * in postgresql.conf.  We don't really want such a dangerous option being
index 2ebd7e85262c686607fc97751976ee8344186b46..22bf5f71c8b51b53a84eef287b9d96e166002526 100644 (file)
@@ -29,6 +29,7 @@
 
 #include "common/fe_memutils.h"
 #include "datatype/timestamp.h"
+#include "fe_utils/connect.h"
 
 const char *progname;
 char      *connection_string = NULL;
@@ -205,6 +206,23 @@ GetConnection(void)
    if (conn_opts)
        PQconninfoFree(conn_opts);
 
+   /* Set always-secure search path, so malicious users can't get control. */
+   if (dbname != NULL)
+   {
+       PGresult   *res;
+
+       res = PQexec(tmpconn, ALWAYS_SECURE_SEARCH_PATH_SQL);
+       if (PQresultStatus(res) != PGRES_TUPLES_OK)
+       {
+           fprintf(stderr, _("%s: could not clear search_path: %s\n"),
+                   progname, PQerrorMessage(tmpconn));
+           PQclear(res);
+           PQfinish(tmpconn);
+           exit(1);
+       }
+       PQclear(res);
+   }
+
    /*
     * Ensure we have the same value of integer timestamps as the server we
     * are connecting to.
index fdf9d47b6448d64093e8ce59114b5621746f29c4..8eb9e0c9926128a1267dc311458687a58efea030 100644 (file)
@@ -1376,8 +1376,9 @@ processSQLNamePattern(PGconn *conn, PQExpBuffer buf, const char *pattern,
    }
 
    /*
-    * Now decide what we need to emit.  Note there will be a leading "^(" in
-    * the patterns in any case.
+    * Now decide what we need to emit.  We may run under a hostile
+    * search_path, so qualify EVERY name.  Note there will be a leading "^("
+    * in the patterns in any case.
     */
    if (namebuf.len > 2)
    {
@@ -1390,15 +1391,18 @@ processSQLNamePattern(PGconn *conn, PQExpBuffer buf, const char *pattern,
            WHEREAND();
            if (altnamevar)
            {
-               appendPQExpBuffer(buf, "(%s ~ ", namevar);
+               appendPQExpBuffer(buf,
+                                 "(%s OPERATOR(pg_catalog.~) ", namevar);
                appendStringLiteralConn(buf, namebuf.data, conn);
-               appendPQExpBuffer(buf, "\n        OR %s ~ ", altnamevar);
+               appendPQExpBuffer(buf,
+                                 "\n        OR %s OPERATOR(pg_catalog.~) ",
+                                 altnamevar);
                appendStringLiteralConn(buf, namebuf.data, conn);
                appendPQExpBufferStr(buf, ")\n");
            }
            else
            {
-               appendPQExpBuffer(buf, "%s ~ ", namevar);
+               appendPQExpBuffer(buf, "%s OPERATOR(pg_catalog.~) ", namevar);
                appendStringLiteralConn(buf, namebuf.data, conn);
                appendPQExpBufferChar(buf, '\n');
            }
@@ -1414,7 +1418,7 @@ processSQLNamePattern(PGconn *conn, PQExpBuffer buf, const char *pattern,
        if (strcmp(schemabuf.data, "^(.*)$") != 0 && schemavar)
        {
            WHEREAND();
-           appendPQExpBuffer(buf, "%s ~ ", schemavar);
+           appendPQExpBuffer(buf, "%s OPERATOR(pg_catalog.~) ", schemavar);
            appendStringLiteralConn(buf, schemabuf.data, conn);
            appendPQExpBufferChar(buf, '\n');
        }
index cbfdf4889e8d1248faba3bc62487f34c34b3b19b..d21b5c483aedfee221b5507dbd97d0444325cf8e 100644 (file)
@@ -10,6 +10,7 @@
  *-------------------------------------------------------------------------
  */
 
+#include "fe_utils/connect.h"
 #include "pg_backup_db.h"
 #include "pg_backup_utils.h"
 #include "dumputils.h"
@@ -96,6 +97,11 @@ ReconnectToServer(ArchiveHandle *AH, const char *dbname, const char *username)
    PQfinish(AH->connection);
    AH->connection = newConn;
 
+   /* Start strict; later phases may override this. */
+   if (PQserverVersion(AH->connection) >= 70300)
+       PQclear(ExecuteSqlQueryForSingleRow((Archive *) AH,
+                                           ALWAYS_SECURE_SEARCH_PATH_SQL));
+
    return 1;
 }
 
@@ -304,6 +310,11 @@ ConnectDatabase(Archive *AHX,
                      PQdb(AH->connection) ? PQdb(AH->connection) : "",
                      PQerrorMessage(AH->connection));
 
+   /* Start strict; later phases may override this. */
+   if (PQserverVersion(AH->connection) >= 70300)
+       PQclear(ExecuteSqlQueryForSingleRow((Archive *) AH,
+                                           ALWAYS_SECURE_SEARCH_PATH_SQL));
+
    /*
     * We want to remember connection's actual password, whether or not we got
     * it by prompting.  So we don't just store the password variable.
index 10607644889669407a277a7e49efe0fdbdd98369..4501c8bed8e1c840c8d2063aaf4d6885029598c3 100644 (file)
@@ -61,6 +61,7 @@
 #include "pg_backup_db.h"
 #include "pg_backup_utils.h"
 #include "dumputils.h"
+#include "fe_utils/connect.h"
 #include "parallel.h"
 
 
@@ -979,6 +980,9 @@ setup_connection(Archive *AH, const char *dumpencoding, char *use_role)
    PGconn     *conn = GetConnection(AH);
    const char *std_strings;
 
+   if (AH->remoteVersion >= 70300)
+       PQclear(ExecuteSqlQueryForSingleRow(AH, ALWAYS_SECURE_SEARCH_PATH_SQL));
+
    /*
     * Set the client encoding if requested.
     */
@@ -1245,13 +1249,20 @@ expand_table_name_patterns(Archive *fout,
 
    for (cell = patterns->head; cell; cell = cell->next)
    {
+       /*
+        * Query must remain ABSOLUTELY devoid of unqualified names.  This
+        * would be unnecessary given a pg_table_is_visible() variant taking a
+        * search_path argument.
+        */
        if (cell != patterns->head)
            appendPQExpBufferStr(query, "UNION ALL\n");
        appendPQExpBuffer(query,
                          "SELECT c.oid"
                          "\nFROM pg_catalog.pg_class c"
-       "\n     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace"
-                    "\nWHERE c.relkind in ('%c', '%c', '%c', '%c', '%c')\n",
+                         "\n     LEFT JOIN pg_catalog.pg_namespace n"
+                         "\n     ON n.oid OPERATOR(pg_catalog.=) c.relnamespace"
+                         "\nWHERE c.relkind OPERATOR(pg_catalog.=) ANY"
+                         "\n    (array['%c', '%c', '%c', '%c', '%c'])\n",
                          RELKIND_RELATION, RELKIND_SEQUENCE, RELKIND_VIEW,
                          RELKIND_MATVIEW, RELKIND_FOREIGN_TABLE);
        processSQLNamePattern(GetConnection(fout), query, cell->val, true,
@@ -1259,7 +1270,9 @@ expand_table_name_patterns(Archive *fout,
                              "pg_catalog.pg_table_is_visible(c.oid)");
    }
 
+   ExecuteSqlStatement(fout, "RESET search_path");
    res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+   PQclear(ExecuteSqlQueryForSingleRow(fout, ALWAYS_SECURE_SEARCH_PATH_SQL));
 
    for (i = 0; i < PQntuples(res); i++)
    {
index 99d10f0c059f6ae69252e640db07a0662d7f89d3..fc2ee0a07e357f7c7d6d2e5e73f0139cb40f39e6 100644 (file)
@@ -26,6 +26,7 @@
 
 #include "dumputils.h"
 #include "pg_backup.h"
+#include "fe_utils/connect.h"
 
 /* version string we expect back from pg_dump */
 #define PGDUMP_VERSIONSTR "pg_dump (PostgreSQL) " PG_VERSION "\n"
@@ -1970,12 +1971,8 @@ connectDatabase(const char *dbname, const char *connection_string,
        exit_nicely(1);
    }
 
-   /*
-    * On 7.3 and later, make sure we are not fooled by non-system schemas in
-    * the search path.
-    */
    if (server_version >= 70300)
-       executeCommand(conn, "SET search_path = pg_catalog");
+       PQclear(executeQuery(conn, ALWAYS_SECURE_SEARCH_PATH_SQL));
 
    return conn;
 }
index eeb2539d372349f20e28a1477600707768425b40..a893920f9ccd57b5e9fe27304056e176630c7dd8 100644 (file)
@@ -25,16 +25,17 @@ all: $(PROGRAMS)
 %: %.o $(WIN32RES)
    $(CC) $(CFLAGS) $^ $(libpq_pgport) $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X)
 
-createdb: createdb.o common.o dumputils.o kwlookup.o keywords.o | submake-libpq submake-libpgport
-createlang: createlang.o common.o print.o mbprint.o | submake-libpq submake-libpgport
-createuser: createuser.o common.o dumputils.o kwlookup.o keywords.o | submake-libpq submake-libpgport
-dropdb: dropdb.o common.o dumputils.o kwlookup.o keywords.o | submake-libpq submake-libpgport
-droplang: droplang.o common.o print.o mbprint.o | submake-libpq submake-libpgport
-dropuser: dropuser.o common.o dumputils.o kwlookup.o keywords.o | submake-libpq submake-libpgport
-clusterdb: clusterdb.o common.o dumputils.o kwlookup.o keywords.o | submake-libpq submake-libpgport
-vacuumdb: vacuumdb.o common.o dumputils.o kwlookup.o keywords.o | submake-libpq submake-libpgport
-reindexdb: reindexdb.o common.o dumputils.o kwlookup.o keywords.o | submake-libpq submake-libpgport
-pg_isready: pg_isready.o common.o | submake-libpq submake-libpgport
+SCRIPTS_COMMON = common.o dumputils.o kwlookup.o keywords.o
+createdb: createdb.o $(SCRIPTS_COMMON) | submake-libpq submake-libpgport
+createlang: createlang.o $(SCRIPTS_COMMON) print.o mbprint.o | submake-libpq submake-libpgport
+createuser: createuser.o $(SCRIPTS_COMMON) | submake-libpq submake-libpgport
+dropdb: dropdb.o $(SCRIPTS_COMMON) | submake-libpq submake-libpgport
+droplang: droplang.o $(SCRIPTS_COMMON) print.o mbprint.o | submake-libpq submake-libpgport
+dropuser: dropuser.o $(SCRIPTS_COMMON) | submake-libpq submake-libpgport
+clusterdb: clusterdb.o $(SCRIPTS_COMMON) | submake-libpq submake-libpgport
+vacuumdb: vacuumdb.o $(SCRIPTS_COMMON) | submake-libpq submake-libpgport
+reindexdb: reindexdb.o $(SCRIPTS_COMMON) | submake-libpq submake-libpgport
+pg_isready: pg_isready.o $(SCRIPTS_COMMON) | submake-libpq submake-libpgport
 
 dumputils.c keywords.c: % : $(top_srcdir)/src/bin/pg_dump/%
    rm -f $@ && $(LN_S) $< .
@@ -65,7 +66,7 @@ uninstall:
 
 clean distclean maintainer-clean:
    rm -f $(addsuffix $(X), $(PROGRAMS)) $(addsuffix .o, $(PROGRAMS))
-   rm -f common.o dumputils.o kwlookup.o keywords.o print.o mbprint.o $(WIN32RES)
+   rm -f $(SCRIPTS_COMMON) print.o mbprint.o $(WIN32RES)
    rm -f dumputils.c print.c mbprint.c kwlookup.c keywords.c
    rm -rf tmp_check
 
index c6d59138862fefcf770d5f2d38aa8e5dad5ce790..ffc99d43c1eb6827721ca4ae7dccd57b23c6d236 100644 (file)
@@ -194,17 +194,21 @@ cluster_one_database(const char *dbname, bool verbose, const char *table,
 
    PGconn     *conn;
 
+   conn = connectDatabase(dbname, host, port, username, prompt_password,
+                          progname, echo, false);
+
    initPQExpBuffer(&sql);
 
    appendPQExpBufferStr(&sql, "CLUSTER");
    if (verbose)
        appendPQExpBufferStr(&sql, " VERBOSE");
    if (table)
-       appendPQExpBuffer(&sql, " %s", table);
-   appendPQExpBufferStr(&sql, ";");
+   {
+       appendPQExpBufferChar(&sql, ' ');
+       appendQualifiedRelation(&sql, table, conn, progname, echo);
+   }
+   appendPQExpBufferChar(&sql, ';');
 
-   conn = connectDatabase(dbname, host, port, username, prompt_password,
-                          progname, false);
    if (!executeMaintenanceCommand(conn, sql.data, echo))
    {
        if (table)
@@ -233,7 +237,7 @@ cluster_all_databases(bool verbose, const char *maintenance_db,
    int         i;
 
    conn = connectMaintenanceDatabase(maintenance_db, host, port, username,
-                                     prompt_password, progname);
+                                     prompt_password, progname, echo);
    result = executeQuery(conn, "SELECT datname FROM pg_database WHERE datallowconn ORDER BY 1;", progname, echo);
    PQfinish(conn);
 
index 311fed5090b89596121813027377a51ad8ad0160..17bb6057f836c4b544dc876dee7228c6cc63a8ae 100644 (file)
@@ -18,6 +18,8 @@
 #include 
 
 #include "common.h"
+#include "dumputils.h"
+#include "fe_utils/connect.h"
 
 static void SetCancelConn(PGconn *conn);
 static void ResetCancelConn(void);
@@ -57,9 +59,10 @@ handle_help_version_opts(int argc, char *argv[],
  * interactive password prompt is automatically issued if required.
  */
 PGconn *
-connectDatabase(const char *dbname, const char *pghost, const char *pgport,
-               const char *pguser, enum trivalue prompt_password,
-               const char *progname, bool fail_ok)
+connectDatabase(const char *dbname, const char *pghost,
+               const char *pgport, const char *pguser,
+               enum trivalue prompt_password, const char *progname,
+               bool echo, bool fail_ok)
 {
    PGconn     *conn;
    char       *password = NULL;
@@ -133,6 +136,10 @@ connectDatabase(const char *dbname, const char *pghost, const char *pgport,
        exit(1);
    }
 
+   if (PQserverVersion(conn) >= 70300)
+       PQclear(executeQuery(conn, ALWAYS_SECURE_SEARCH_PATH_SQL,
+                            progname, echo));
+
    return conn;
 }
 
@@ -140,24 +147,24 @@ connectDatabase(const char *dbname, const char *pghost, const char *pgport,
  * Try to connect to the appropriate maintenance database.
  */
 PGconn *
-connectMaintenanceDatabase(const char *maintenance_db, const char *pghost,
-                          const char *pgport, const char *pguser,
-                          enum trivalue prompt_password,
-                          const char *progname)
+connectMaintenanceDatabase(const char *maintenance_db,
+                          const char *pghost, const char *pgport,
+                          const char *pguser, enum trivalue prompt_password,
+                          const char *progname, bool echo)
 {
    PGconn     *conn;
 
    /* If a maintenance database name was specified, just connect to it. */
    if (maintenance_db)
        return connectDatabase(maintenance_db, pghost, pgport, pguser,
-                              prompt_password, progname, false);
+                              prompt_password, progname, echo, false);
 
    /* Otherwise, try postgres first and then template1. */
    conn = connectDatabase("postgres", pghost, pgport, pguser, prompt_password,
-                          progname, true);
+                          progname, echo, true);
    if (!conn)
        conn = connectDatabase("template1", pghost, pgport, pguser,
-                              prompt_password, progname, false);
+                              prompt_password, progname, echo, false);
 
    return conn;
 }
@@ -243,6 +250,116 @@ executeMaintenanceCommand(PGconn *conn, const char *query, bool echo)
    return r;
 }
 
+
+/*
+ * Split TABLE[(COLUMNS)] into TABLE and [(COLUMNS)] portions.  When you
+ * finish using them, pg_free(*table).  *columns is a pointer into "spec",
+ * possibly to its NUL terminator.
+ */
+static void
+split_table_columns_spec(const char *spec, int encoding,
+                        char **table, const char **columns)
+{
+   bool        inquotes = false;
+   const char *cp = spec;
+
+   /*
+    * Find the first '(' not identifier-quoted.  Based on
+    * dequote_downcase_identifier().
+    */
+   while (*cp && (*cp != '(' || inquotes))
+   {
+       if (*cp == '"')
+       {
+           if (inquotes && cp[1] == '"')
+               cp++;           /* pair does not affect quoting */
+           else
+               inquotes = !inquotes;
+           cp++;
+       }
+       else
+           cp += PQmblen(cp, encoding);
+   }
+   *table = pg_strdup(spec);
+   (*table)[cp - spec] = '\0'; /* no strndup */
+   *columns = cp;
+}
+
+/*
+ * Break apart TABLE[(COLUMNS)] of "spec".  With the reset_val of search_path
+ * in effect, have regclassin() interpret the TABLE portion.  Append to "buf"
+ * the qualified name of TABLE, followed by any (COLUMNS).  Exit on failure.
+ * We use this to interpret --table=foo under the search path psql would get,
+ * in advance of "ANALYZE public.foo" under the always-secure search path.
+ */
+void
+appendQualifiedRelation(PQExpBuffer buf, const char *spec,
+                       PGconn *conn, const char *progname, bool echo)
+{
+   char       *table;
+   const char *columns;
+   PQExpBufferData sql;
+   PGresult   *res;
+   int         ntups;
+
+   /* Before 7.3, the concept of qualifying a name did not exist. */
+   if (PQserverVersion(conn) < 70300)
+   {
+       appendPQExpBufferStr(&sql, spec);
+       return;
+   }
+
+   split_table_columns_spec(spec, PQclientEncoding(conn), &table, &columns);
+
+   /*
+    * Query must remain ABSOLUTELY devoid of unqualified names.  This would
+    * be unnecessary given a regclassin() variant taking a search_path
+    * argument.
+    */
+   initPQExpBuffer(&sql);
+   appendPQExpBufferStr(&sql,
+                        "SELECT c.relname, ns.nspname\n"
+                        " FROM pg_catalog.pg_class c,"
+                        " pg_catalog.pg_namespace ns\n"
+                        " WHERE c.relnamespace OPERATOR(pg_catalog.=) ns.oid\n"
+                        "  AND c.oid OPERATOR(pg_catalog.=) ");
+   appendStringLiteralConn(&sql, table, conn);
+   appendPQExpBufferStr(&sql, "::pg_catalog.regclass;");
+
+   executeCommand(conn, "RESET search_path", progname, echo);
+
+   /*
+    * One row is a typical result, as is a nonexistent relation ERROR.
+    * regclassin() unconditionally accepts all-digits input as an OID; if no
+    * relation has that OID; this query returns no rows.  Catalog corruption
+    * might elicit other row counts.
+    */
+   res = executeQuery(conn, sql.data, progname, echo);
+   ntups = PQntuples(res);
+   if (ntups != 1)
+   {
+       fprintf(stderr,
+               ngettext("%s: query returned %d row instead of one: %s\n",
+                        "%s: query returned %d rows instead of one: %s\n",
+                        ntups),
+               progname, ntups, sql.data);
+       PQfinish(conn);
+       exit(1);
+   }
+   appendPQExpBufferStr(buf,
+                        fmtQualifiedId(PQserverVersion(conn),
+                                       PQgetvalue(res, 0, 1),
+                                       PQgetvalue(res, 0, 0)));
+   appendPQExpBufferStr(buf, columns);
+   PQclear(res);
+   termPQExpBuffer(&sql);
+   pg_free(table);
+
+   PQclear(executeQuery(conn, ALWAYS_SECURE_SEARCH_PATH_SQL,
+                        progname, echo));
+}
+
+
 /*
  * Check yes/no answer in a localized way.  1=yes, 0=no, -1=neither.
  */
index 691f6c6a02f43a909446e8918e2d4e7b5d071bd4..4067af3455604aa9b09fd1f441db5b3da560cfe3 100644 (file)
@@ -30,11 +30,12 @@ extern void handle_help_version_opts(int argc, char *argv[],
 extern PGconn *connectDatabase(const char *dbname, const char *pghost,
                const char *pgport, const char *pguser,
                enum trivalue prompt_password, const char *progname,
-               bool fail_ok);
+               bool echo, bool fail_ok);
 
 extern PGconn *connectMaintenanceDatabase(const char *maintenance_db,
-                 const char *pghost, const char *pgport, const char *pguser,
-                       enum trivalue prompt_password, const char *progname);
+                          const char *pghost, const char *pgport,
+                          const char *pguser, enum trivalue prompt_password,
+                          const char *progname, bool echo);
 
 extern PGresult *executeQuery(PGconn *conn, const char *query,
             const char *progname, bool echo);
@@ -45,6 +46,9 @@ extern void executeCommand(PGconn *conn, const char *query,
 extern bool executeMaintenanceCommand(PGconn *conn, const char *query,
                          bool echo);
 
+extern void appendQualifiedRelation(PQExpBuffer buf, const char *name,
+                       PGconn *conn, const char *progname, bool echo);
+
 extern bool yesno_prompt(const char *question);
 
 extern void setup_cancel_handler(void);
index 64badd6bd30029caa50ebbdb3ac4be5da9fc9558..c47ef3f0016698177610bf3b8b4faef33f353a79 100644 (file)
@@ -202,7 +202,7 @@ main(int argc, char *argv[])
        maintenance_db = "template1";
 
    conn = connectMaintenanceDatabase(maintenance_db, host, port, username,
-                                     prompt_password, progname);
+                                     prompt_password, progname, echo);
 
    if (echo)
        printf("%s\n", sql.data);
index 9a7adfa72c6ad0623d741e765117b7e94433b6b3..7f16bcaf4038f01316f78527963ec2d7cb2e5447 100644 (file)
@@ -141,7 +141,7 @@ main(int argc, char *argv[])
        static const bool translate_columns[] = {false, true};
 
        conn = connectDatabase(dbname, host, port, username, prompt_password,
-                              progname, false);
+                              progname, echo, false);
 
        printfPQExpBuffer(&sql, "SELECT lanname as \"%s\", "
                "(CASE WHEN lanpltrusted THEN '%s' ELSE '%s' END) as \"%s\" "
@@ -181,7 +181,7 @@ main(int argc, char *argv[])
            *p += ('a' - 'A');
 
    conn = connectDatabase(dbname, host, port, username, prompt_password,
-                          progname, false);
+                          progname, echo, false);
 
    /*
     * Make sure the language isn't already installed
index 2d49bc2f1e8b7f24829252d161f0c09d4febc62c..1b192af678090af00f361c0cf1cdbf54d58aad58 100644 (file)
@@ -251,7 +251,7 @@ main(int argc, char *argv[])
        login = TRI_YES;
 
    conn = connectDatabase("postgres", host, port, username, prompt_password,
-                          progname, false);
+                          progname, echo, false);
 
    initPQExpBuffer(&sql);
 
index e750b449d6bb571432623b98c4ad727cbe9f0e4f..6d3db8ebae1d13bf9c46443afb970439a145dec4 100644 (file)
@@ -129,7 +129,8 @@ main(int argc, char *argv[])
        maintenance_db = "template1";
 
    conn = connectMaintenanceDatabase(maintenance_db,
-                           host, port, username, prompt_password, progname);
+                                     host, port, username, prompt_password,
+                                     progname, echo);
 
    if (echo)
        printf("%s\n", sql.data);
index 60b56d5e533bce748d4e8151903ed855e764d4a2..84f115f891a07afdd7a88d68d60ee98baf0e23ec 100644 (file)
@@ -140,7 +140,7 @@ main(int argc, char *argv[])
        static const bool translate_columns[] = {false, true};
 
        conn = connectDatabase(dbname, host, port, username, prompt_password,
-                              progname, false);
+                              progname, echo, false);
 
        printfPQExpBuffer(&sql, "SELECT lanname as \"%s\", "
                "(CASE WHEN lanpltrusted THEN '%s' ELSE '%s' END) as \"%s\" "
@@ -182,13 +182,7 @@ main(int argc, char *argv[])
            *p += ('a' - 'A');
 
    conn = connectDatabase(dbname, host, port, username, prompt_password,
-                          progname, false);
-
-   /*
-    * Force schema search path to be just pg_catalog, so that we don't have
-    * to be paranoid about search paths below.
-    */
-   executeCommand(conn, "SET search_path = pg_catalog;", progname, echo);
+                          progname, echo, false);
 
    /*
     * Make sure the language is installed
index 69a5a493ae3ae0e5af5f6b641b58e2172cf1367a..b92559d94bec2987c12d7933b8936efa33c4706f 100644 (file)
@@ -129,7 +129,7 @@ main(int argc, char *argv[])
                      (if_exists ? "IF EXISTS " : ""), fmtId(dropuser));
 
    conn = connectDatabase("postgres", host, port, username, prompt_password,
-                          progname, false);
+                          progname, echo, false);
 
    if (echo)
        printf("%s\n", sql.data);
index a868ed75855abc4b96ab46eb4c44419054576d51..c95c1fff152e2394778976ff98043931cd947b19 100644 (file)
@@ -245,18 +245,19 @@ reindex_one_database(const char *name, const char *dbname, const char *type,
    PGconn     *conn;
 
    conn = connectDatabase(dbname, host, port, username, prompt_password,
-                          progname, false);
+                          progname, echo, false);
 
    initPQExpBuffer(&sql);
 
-   appendPQExpBufferStr(&sql, "REINDEX");
-   if (strcmp(type, "TABLE") == 0)
-       appendPQExpBuffer(&sql, " TABLE %s", name);
-   else if (strcmp(type, "INDEX") == 0)
-       appendPQExpBuffer(&sql, " INDEX %s", name);
+   appendPQExpBufferStr(&sql, "REINDEX ");
+   appendPQExpBufferStr(&sql, type);
+   appendPQExpBufferChar(&sql, ' ');
+   if (strcmp(type, "TABLE") == 0 ||
+       strcmp(type, "INDEX") == 0)
+       appendQualifiedRelation(&sql, name, conn, progname, echo);
    else if (strcmp(type, "DATABASE") == 0)
-       appendPQExpBuffer(&sql, " DATABASE %s", fmtId(PQdb(conn)));
-   appendPQExpBufferStr(&sql, ";");
+       appendPQExpBufferStr(&sql, fmtId(PQdb(conn)));
+   appendPQExpBufferChar(&sql, ';');
 
    if (!executeMaintenanceCommand(conn, sql.data, echo))
    {
@@ -289,7 +290,7 @@ reindex_all_databases(const char *maintenance_db,
    int         i;
 
    conn = connectMaintenanceDatabase(maintenance_db, host, port, username,
-                                     prompt_password, progname);
+                                     prompt_password, progname, echo);
    result = executeQuery(conn, "SELECT datname FROM pg_database WHERE datallowconn ORDER BY 1;", progname, echo);
    PQfinish(conn);
 
@@ -326,7 +327,7 @@ reindex_system_catalogs(const char *dbname, const char *host, const char *port,
    PQExpBufferData sql;
 
    conn = connectDatabase(dbname, host, port, username, prompt_password,
-                          progname, false);
+                          progname, echo, false);
 
    initPQExpBuffer(&sql);
 
index dc0d78a27d374edecf3682c3e19ae16ec8721fdd..bdb32734510f1d4192191462d6c8bbebfe2cbc48 100644 (file)
@@ -22,5 +22,5 @@ psql 'postgres',
 'CREATE TABLE test1 (a int); CREATE INDEX test1x ON test1 (a); CLUSTER test1 USING test1x';
 issues_sql_like(
    [ 'clusterdb', '-t', 'test1', 'postgres' ],
-   qr/statement: CLUSTER test1;/,
+   qr/statement: CLUSTER public\.test1;/,
    'cluster specific table');
index bba1667b56f505a9c60ed37e5562e3f3682af02a..54864b2209e06c10f3d573c32715b1b6fad08699 100644 (file)
@@ -21,11 +21,11 @@ psql 'postgres',
   'CREATE TABLE test1 (a int); CREATE INDEX test1x ON test1 (a);';
 issues_sql_like(
    [ 'reindexdb', '-t', 'test1', 'postgres' ],
-   qr/statement: REINDEX TABLE test1;/,
+   qr/statement: REINDEX TABLE public\.test1;/,
    'reindex specific table');
 issues_sql_like(
    [ 'reindexdb', '-i', 'test1x', 'postgres' ],
-   qr/statement: REINDEX INDEX test1x;/,
+   qr/statement: REINDEX INDEX public\.test1x;/,
    'reindex specific index');
 issues_sql_like(
    [ 'reindexdb', '-s', 'postgres' ],
index ac160ba8374502c095caa8540acf1f10a942bc06..bdd03bc2449aafaae2b66215d6dcaae9468915cc 100644 (file)
@@ -1,7 +1,7 @@
 use strict;
 use warnings;
 use TestLib;
-use Test::More tests => 18;
+use Test::More tests => 23;
 
 program_help_ok('vacuumdb');
 program_version_ok('vacuumdb');
@@ -30,3 +30,25 @@ issues_sql_like(
    [ 'vacuumdb', '-Z', 'postgres' ],
    qr/statement: ANALYZE;/,
    'vacuumdb -Z');
+command_ok([qw(vacuumdb -Z --table=pg_am dbname=template1)],
+   'vacuumdb with connection string');
+
+command_fails([qw(vacuumdb -Zt pg_am;ABORT postgres)],
+   'trailing command in "-t", without COLUMNS');
+# Unwanted; better if it failed.
+command_ok([qw(vacuumdb -Zt pg_am(amname);ABORT postgres)],
+   'trailing command in "-t", with COLUMNS');
+
+psql('postgres', q|
+  CREATE TABLE "need""q(uot" (")x" text);
+
+  CREATE FUNCTION f0(int) RETURNS int LANGUAGE SQL AS 'SELECT $1 * $1';
+  CREATE FUNCTION f1(int) RETURNS int LANGUAGE SQL AS 'SELECT f0($1)';
+  CREATE TABLE funcidx (x int);
+  INSERT INTO funcidx VALUES (0),(1),(2),(3);
+  CREATE INDEX i0 ON funcidx ((f1(x)));
+|);
+command_ok([qw|vacuumdb -Z --table="need""q(uot"(")x") postgres|],
+   'column list');
+command_fails([qw|vacuumdb -Zt funcidx postgres|],
+   'unqualifed name via functional index');
index eac5bfe567118b25397160482e857a3ed8157be8..25567777070e2e079b9eb3499e705c4cdbf14e85 100644 (file)
@@ -266,7 +266,7 @@ vacuum_one_database(const char *dbname, bool full, bool verbose, bool and_analyz
    initPQExpBuffer(&sql);
 
    conn = connectDatabase(dbname, host, port, username, prompt_password,
-                          progname, false);
+                          progname, echo, false);
 
    if (analyze_only)
    {
@@ -319,7 +319,10 @@ vacuum_one_database(const char *dbname, bool full, bool verbose, bool and_analyz
        }
    }
    if (table)
-       appendPQExpBuffer(&sql, " %s", table);
+   {
+       appendPQExpBufferChar(&sql, ' ');
+       appendQualifiedRelation(&sql, table, conn, progname, echo);
+   }
    appendPQExpBufferStr(&sql, ";");
 
    if (analyze_in_stages)
@@ -386,7 +389,7 @@ vacuum_all_databases(bool full, bool verbose, bool and_analyze, bool analyze_onl
    int         stage;
 
    conn = connectMaintenanceDatabase(maintenance_db, host, port,
-                                     username, prompt_password, progname);
+                                     username, prompt_password, progname, echo);
    result = executeQuery(conn, "SELECT datname FROM pg_database WHERE datallowconn ORDER BY 1;", progname, echo);
    PQfinish(conn);
 
index 578a778461615c27adc0941282c8487fce750270..9bba3082a98efafe15d376524cf54f8b8c5d3e34 100644 (file)
@@ -17,7 +17,8 @@ all: pg_config.h pg_config_ext.h pg_config_os.h
 
 
 # Subdirectories containing headers for server-side dev
-SUBDIRS = access bootstrap catalog commands common datatype executor foreign \
+SUBDIRS = access bootstrap catalog commands common datatype \
+   executor fe_utils foreign \
    lib libpq mb nodes optimizer parser postmaster regex replication \
    rewrite storage tcop snowball snowball/libstemmer tsearch \
    tsearch/dicts utils port port/win32 port/win32_msvc \
diff --git a/src/include/fe_utils/connect.h b/src/include/fe_utils/connect.h
new file mode 100644 (file)
index 0000000..fa293d2
--- /dev/null
@@ -0,0 +1,28 @@
+/*-------------------------------------------------------------------------
+ *
+ * Interfaces in support of FE/BE connections.
+ *
+ *
+ * Portions Copyright (c) 1996-2018, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/fe_utils/connect.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef CONNECT_H
+#define CONNECT_H
+
+/*
+ * This SQL statement installs an always-secure search path, so malicious
+ * users can't take control.  CREATE of an unqualified name will fail, because
+ * this selects no creation schema.  This does not demote pg_temp, so it is
+ * suitable where we control the entire FE/BE connection but not suitable in
+ * SECURITY DEFINER functions.  This is portable to PostgreSQL 7.3, which
+ * introduced schemas.  When connected to an older version from code that
+ * might work with the old server, skip this.
+ */
+#define ALWAYS_SECURE_SEARCH_PATH_SQL \
+   "SELECT pg_catalog.set_config('search_path', '', false)"
+
+#endif                         /* CONNECT_H */
index 989e992edb41bb2c5e4ac439025ba5703ccd4e23..512d19ad4b9333e9fcd94847cf8c41c2a7d35a2b 100644 (file)
@@ -7,6 +7,7 @@
  */
 #include "postgres_fe.h"
 
+#include "fe_utils/connect.h"
 #include "libpq-fe.h"
 #include "pqexpbuffer.h"
 
@@ -44,6 +45,14 @@ main(int argc, char **argv)
        exit(EXIT_FAILURE);
    }
 
+   res = PQexec(conn, ALWAYS_SECURE_SEARCH_PATH_SQL);
+   if (!res || PQresultStatus(res) != PGRES_TUPLES_OK)
+   {
+       fprintf(stderr, "sql error:  %s\n", PQerrorMessage(conn));
+       exit(EXIT_FAILURE);
+   }
+   PQclear(res);
+
    /* Get a list of relations that have OIDs */
 
    printfPQExpBuffer(&sql, "%s",