diff options
author | Noah Misch <noah@leadboat.com> | 2018-02-26 07:39:44 -0800 |
---|---|---|
committer | Noah Misch <noah@leadboat.com> | 2018-02-26 07:39:48 -0800 |
commit | 928bca1a30d7e05cc3857a99e27aa8ed08ed2fac (patch) | |
tree | 4cde95cae4c2a06fd249123078e4b4255d00d17f /src/bin/scripts/common.c | |
parent | 461c32b557ddbb0ed67b4b2232a191554ad40c3c (diff) | |
download | postgresql-928bca1a30d7e05cc3857a99e27aa8ed08ed2fac.tar.gz postgresql-928bca1a30d7e05cc3857a99e27aa8ed08ed2fac.zip |
Empty search_path in Autovacuum and non-psql/pgbench clients.
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
Diffstat (limited to 'src/bin/scripts/common.c')
-rw-r--r-- | src/bin/scripts/common.c | 137 |
1 files changed, 127 insertions, 10 deletions
diff --git a/src/bin/scripts/common.c b/src/bin/scripts/common.c index 311fed5090b..17bb6057f83 100644 --- a/src/bin/scripts/common.c +++ b/src/bin/scripts/common.c @@ -18,6 +18,8 @@ #include <unistd.h> #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. */ |