\gsetenv

Started by David Fetterabout 5 years ago12 messages
#1David Fetter
david@fetter.org
1 attachment(s)

Hi,

We have \gset to set some parameters, but not ones in the environment,
so I fixed this with a new analogous command, \gsetenv. I considered
refactoring SetVariable to include environment variables, but for a
first cut, I just made a separate function and an extra if.

Best,
David.
--
David Fetter <david(at)fetter(dot)org> http://fetter.org/
Phone: +1 415 235 3778

Remember to vote!
Consider donating to Postgres: http://www.postgresql.org/about/donate

Attachments:

v1-0001-Implement-gsetenv-similar-to-gset.patchtext/x-diff; charset=us-asciiDownload
From 04059e68ffcd8cf4052ccb6a013f0cf2e0095eb8 Mon Sep 17 00:00:00 2001
From: David Fetter <david@fetter.org>
Date: Wed, 16 Dec 2020 13:17:32 -0800
Subject: [PATCH v1] Implement \gsetenv, similar to \gset
To: hackers
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="------------2.28.0"

This is a multi-part message in MIME format.
--------------2.28.0
Content-Type: text/plain; charset=UTF-8; format=fixed
Content-Transfer-Encoding: 8bit


In passing, make \setenv without arguments dump the environment in a way
similar to what bare \set and \pset do.

diff --git doc/src/sgml/ref/psql-ref.sgml doc/src/sgml/ref/psql-ref.sgml
index 221a967bfe..97d5a2ed19 100644
--- doc/src/sgml/ref/psql-ref.sgml
+++ doc/src/sgml/ref/psql-ref.sgml
@@ -2297,6 +2297,49 @@ hello 10
         </listitem>
       </varlistentry>
 
+      <varlistentry>
+        <term><literal>\gsetenv [ <replaceable class="parameter">prefix</replaceable> ]</literal></term>
+
+        <listitem>
+        <para>
+         Sends the current query buffer to the server and stores the
+         query's output into environment variables.
+         The query to be executed must return exactly one row.  Each column of
+         the row is stored into a separate variable, named the same as the
+         column.  For example:
+<programlisting>
+=&gt; <userinput>SELECT 'hello' AS var1, 10 AS var2</userinput>
+-&gt; <userinput>\gsetenv</userinput>
+=&gt; <userinput>\! echo $var1 $var2</userinput>
+hello 10
+</programlisting>
+        </para>
+        <para>
+         If you specify a <replaceable class="parameter">prefix</replaceable>,
+         that string is prepended to the query's column names to create the
+         variable names to use:
+<programlisting>
+=&gt; <userinput>SELECT 'hello' AS var1, 10 AS var2</userinput>
+-&gt; <userinput>\gsetenv result_</userinput>
+=&gt; <userinput>\! echo $result_var1 $result_var2</userinput>
+hello 10
+</programlisting>
+        </para>
+        <para>
+         If a column result is NULL, the corresponding environment variable is unset
+         rather than being set.
+        </para>
+        <para>
+         If the query fails or does not return one row,
+         no variables are changed.
+        </para>
+        <para>
+         If the current query buffer is empty, the most recently sent query
+         is re-executed instead.
+        </para>
+        </listitem>
+      </varlistentry>
+
 
       <varlistentry>
         <term><literal>\gx [ (<replaceable class="parameter">option</replaceable>=<replaceable class="parameter">value</replaceable> [...]) ] [ <replaceable class="parameter">filename</replaceable> ]</literal></term>
diff --git src/bin/psql/command.c src/bin/psql/command.c
index 38b588882d..8b8749aca6 100644
--- src/bin/psql/command.c
+++ src/bin/psql/command.c
@@ -94,7 +94,9 @@ static backslashResult process_command_g_options(char *first_option,
 												 const char *cmd);
 static backslashResult exec_command_gdesc(PsqlScanState scan_state, bool active_branch);
 static backslashResult exec_command_gexec(PsqlScanState scan_state, bool active_branch);
-static backslashResult exec_command_gset(PsqlScanState scan_state, bool active_branch);
+static backslashResult exec_command_gset(PsqlScanState scan_state,
+										 bool active_branch,
+										 GSET_TARGET gset_target);
 static backslashResult exec_command_help(PsqlScanState scan_state, bool active_branch);
 static backslashResult exec_command_html(PsqlScanState scan_state, bool active_branch);
 static backslashResult exec_command_include(PsqlScanState scan_state, bool active_branch,
@@ -346,7 +348,9 @@ exec_command(const char *cmd,
 	else if (strcmp(cmd, "gexec") == 0)
 		status = exec_command_gexec(scan_state, active_branch);
 	else if (strcmp(cmd, "gset") == 0)
-		status = exec_command_gset(scan_state, active_branch);
+		status = exec_command_gset(scan_state, active_branch, GSET_TARGET_PSET);
+	else if (strcmp(cmd, "gsetenv") == 0)
+		status = exec_command_gset(scan_state, active_branch, GSET_TARGET_ENV);
 	else if (strcmp(cmd, "h") == 0 || strcmp(cmd, "help") == 0)
 		status = exec_command_help(scan_state, active_branch);
 	else if (strcmp(cmd, "H") == 0 || strcmp(cmd, "html") == 0)
@@ -1451,10 +1455,11 @@ exec_command_gexec(PsqlScanState scan_state, bool active_branch)
 }
 
 /*
- * \gset [prefix] -- send query and store result into variables
+ * \gset[env] [prefix] -- send query and store result into variables
  */
 static backslashResult
-exec_command_gset(PsqlScanState scan_state, bool active_branch)
+exec_command_gset(PsqlScanState scan_state, bool active_branch,
+				  GSET_TARGET gset_target)
 {
 	backslashResult status = PSQL_CMD_SKIP_LINE;
 
@@ -1463,6 +1468,8 @@ exec_command_gset(PsqlScanState scan_state, bool active_branch)
 		char	   *prefix = psql_scan_slash_option(scan_state,
 													OT_NORMAL, NULL, false);
 
+		pset.gset_target = gset_target;
+
 		if (prefix)
 			pset.gset_prefix = prefix;
 		else
@@ -2275,39 +2282,8 @@ exec_command_setenv(PsqlScanState scan_state, bool active_branch,
 													OT_NORMAL, NULL, false);
 		char	   *envval = psql_scan_slash_option(scan_state,
 													OT_NORMAL, NULL, false);
+		success = SetEnvVariable(cmd, envvar, envval);
 
-		if (!envvar)
-		{
-			pg_log_error("\\%s: missing required argument", cmd);
-			success = false;
-		}
-		else if (strchr(envvar, '=') != NULL)
-		{
-			pg_log_error("\\%s: environment variable name must not contain \"=\"",
-						 cmd);
-			success = false;
-		}
-		else if (!envval)
-		{
-			/* No argument - unset the environment variable */
-			unsetenv(envvar);
-			success = true;
-		}
-		else
-		{
-			/* Set variable to the value of the next argument */
-			char	   *newval;
-
-			newval = psprintf("%s=%s", envvar, envval);
-			putenv(newval);
-			success = true;
-
-			/*
-			 * Do not free newval here, it will screw up the environment if
-			 * you do. See putenv man page for details. That means we leak a
-			 * bit of memory here, but not enough to worry about.
-			 */
-		}
 		free(envvar);
 		free(envval);
 	}
diff --git src/bin/psql/common.c src/bin/psql/common.c
index dfbc22970f..2988613a62 100644
--- src/bin/psql/common.c
+++ src/bin/psql/common.c
@@ -28,6 +28,7 @@
 #include "fe_utils/string_utils.h"
 #include "portability/instr_time.h"
 #include "settings.h"
+#include "variables.h"
 
 static bool DescribeQuery(const char *query, double *elapsed_msec);
 static bool ExecQueryUsingCursor(const char *query, double *elapsed_msec);
@@ -762,15 +763,24 @@ static bool
 StoreQueryTuple(const PGresult *result)
 {
 	bool		success = true;
+	char		*gset_suffix;
+
+	if (pset.gset_target == GSET_TARGET_PSET)
+		gset_suffix = "";
+	else if (pset.gset_target == GSET_TARGET_ENV)
+		gset_suffix = "env";
+	else
+		pg_log_error("How did you manage to get gset_target to be \"%d\%?!?",
+					 pset.gset_target);
 
 	if (PQntuples(result) < 1)
 	{
-		pg_log_error("no rows returned for \\gset");
+		pg_log_error("no rows returned for \\gset%s", gset_suffix);
 		success = false;
 	}
 	else if (PQntuples(result) > 1)
 	{
-		pg_log_error("more than one row returned for \\gset");
+		pg_log_error("more than one row returned for \\gset%s", gset_suffix);
 		success = false;
 	}
 	else
@@ -786,7 +796,7 @@ StoreQueryTuple(const PGresult *result)
 			/* concatenate prefix and column name */
 			varname = psprintf("%s%s", pset.gset_prefix, colname);
 
-			if (VariableHasHook(pset.vars, varname))
+			if (pset.gset_target == GSET_TARGET_PSET && VariableHasHook(pset.vars, varname))
 			{
 				pg_log_warning("attempt to \\gset into specially treated variable \"%s\" ignored",
 							   varname);
@@ -801,14 +811,31 @@ StoreQueryTuple(const PGresult *result)
 				value = NULL;
 			}
 
-			if (!SetVariable(pset.vars, varname, value))
+			if (pset.gset_target == GSET_TARGET_PSET)
 			{
+				if (!SetVariable(pset.vars, varname, value))
+				{
+					free(varname);
+					success = false;
+					continue;
+				}
+
 				free(varname);
-				success = false;
-				break;
 			}
+			else /* Only GSET_TARGET_ENV is possible now */
+			{
+				const char *cmd;
+
+				cmd = "gsetenv";
+				if (!SetEnvVariable(cmd, varname, value))
+				{
+					free(varname);
+					success = false;
+					continue;
+				}
 
-			free(varname);
+				free(varname);
+			}
 		}
 	}
 
diff --git src/bin/psql/help.c src/bin/psql/help.c
index af829282e6..2c135f4abb 100644
--- src/bin/psql/help.c
+++ src/bin/psql/help.c
@@ -180,6 +180,7 @@ slashUsage(unsigned short int pager)
 	fprintf(output, _("  \\gdesc                 describe result of query, without executing it\n"));
 	fprintf(output, _("  \\gexec                 execute query, then execute each value in its result\n"));
 	fprintf(output, _("  \\gset [PREFIX]         execute query and store results in psql variables\n"));
+	fprintf(output, _("  \\gsetenv [PREFIX]      execute query and store results in environment variables\n"));
 	fprintf(output, _("  \\gx [(OPTIONS)] [FILE] as \\g, but forces expanded output mode\n"));
 	fprintf(output, _("  \\q                     quit psql\n"));
 	fprintf(output, _("  \\watch [SEC]           execute query every SEC seconds\n"));
@@ -309,7 +310,7 @@ slashUsage(unsigned short int pager)
 
 	fprintf(output, _("Operating System\n"));
 	fprintf(output, _("  \\cd [DIR]              change the current working directory\n"));
-	fprintf(output, _("  \\setenv NAME [VALUE]   set or unset environment variable\n"));
+	fprintf(output, _("  \\setenv NAME [VALUE]   set environment variable, or list all if no parameters\n"));
 	fprintf(output, _("  \\timing [on|off]       toggle timing of commands (currently %s)\n"),
 			ON(pset.timing));
 	fprintf(output, _("  \\! [COMMAND]           execute command in shell or start interactive shell\n"));
diff --git src/bin/psql/settings.h src/bin/psql/settings.h
index 9601f6e90c..f83e1324aa 100644
--- src/bin/psql/settings.h
+++ src/bin/psql/settings.h
@@ -62,6 +62,13 @@ typedef enum
 	PSQL_COMP_CASE_LOWER
 } PSQL_COMP_CASE;
 
+typedef enum
+{
+	GSET_TARGET_NONE,
+	GSET_TARGET_PSET,
+	GSET_TARGET_ENV
+} GSET_TARGET;
+
 typedef enum
 {
 	hctl_none = 0,
@@ -93,7 +100,7 @@ typedef struct _psqlSettings
 	char	   *gfname;			/* one-shot file output argument for \g */
 	printQueryOpt *gsavepopt;	/* if not null, saved print format settings */
 
-	char	   *gset_prefix;	/* one-shot prefix argument for \gset */
+	char	   *gset_prefix;	/* one-shot prefix argument for \gset* */
 	bool		gdesc_flag;		/* one-shot request to describe query results */
 	bool		gexec_flag;		/* one-shot request to execute query results */
 	bool		crosstab_flag;	/* one-shot request to crosstab results */
@@ -142,6 +149,7 @@ typedef struct _psqlSettings
 	PSQL_ECHO_HIDDEN echo_hidden;
 	PSQL_ERROR_ROLLBACK on_error_rollback;
 	PSQL_COMP_CASE comp_case;
+	GSET_TARGET gset_target;
 	HistControl histcontrol;
 	const char *prompt1;
 	const char *prompt2;
diff --git src/bin/psql/tab-complete.c src/bin/psql/tab-complete.c
index 3a43c09bf6..924abd150b 100644
--- src/bin/psql/tab-complete.c
+++ src/bin/psql/tab-complete.c
@@ -1504,7 +1504,7 @@ psql_completion(const char *text, int start, int end)
 		"\\e", "\\echo", "\\ef", "\\elif", "\\else", "\\encoding",
 		"\\endif", "\\errverbose", "\\ev",
 		"\\f",
-		"\\g", "\\gdesc", "\\gexec", "\\gset", "\\gx",
+		"\\g", "\\gdesc", "\\gexec", "\\gset", "\\gsetenv", "\\gx",
 		"\\h", "\\help", "\\H",
 		"\\i", "\\if", "\\ir",
 		"\\l", "\\lo_import", "\\lo_export", "\\lo_list", "\\lo_unlink",
diff --git src/bin/psql/variables.c src/bin/psql/variables.c
index 84349fc753..26961d8c23 100644
--- src/bin/psql/variables.c
+++ src/bin/psql/variables.c
@@ -5,6 +5,8 @@
  *
  * src/bin/psql/variables.c
  */
+#include <unistd.h>
+
 #include "postgres_fe.h"
 
 #include "common.h"
@@ -295,6 +297,67 @@ SetVariable(VariableSpace space, const char *name, const char *value)
 	return true;
 }
 
+/*
+ * Common code for of \{,g}setenv
+ *
+ * Set the environment variable named "name" to value "value",
+ * or delete it if "value" is NULL.
+ */
+bool SetEnvVariable(const char *cmd, const char *name, const char *value)
+{
+	bool	success = true;
+	if (!name)
+	{
+		int cmp;
+
+		cmp = strcmp(cmd, "setenv");
+
+		if (cmp == 0)
+		{
+			int i;
+
+			for (i = 0; environ[i] != NULL; i++)
+			{
+				printf("%-24s\n", environ[i]);
+			}
+			success = true;
+		}
+		else
+		{
+			pg_log_error("\\%s: missing required argument", cmd);
+			success = false;
+		}
+	}
+	else if (strchr(name, '=') != NULL)
+	{
+		pg_log_error("\\%s: environment variable name must not contain \"=\"",
+					 cmd);
+		success = false;
+	}
+	else if (!value)
+	{
+		/* No argument - unset the environment variable */
+		unsetenv(name);
+		success = true;
+	}
+	else
+	{
+		/* Set variable to the value of the next argument */
+		char	   *newval;
+
+		newval = psprintf("%s=%s", name, value);
+		putenv(newval);
+		success = true;
+
+		/*
+		 * Do not free newvalue here, as it may screw up the environment if
+		 * you do. See putenv man page for details. That means we leak a
+		 * bit of memory here, but not enough to worry about.
+		 */
+	}
+	return success;
+}
+
 /*
  * Attach substitute and/or assign hook functions to the named variable.
  * If you need only one hook, pass NULL for the other.
diff --git src/bin/psql/variables.h src/bin/psql/variables.h
index b932472021..3f54648fd2 100644
--- src/bin/psql/variables.h
+++ src/bin/psql/variables.h
@@ -86,6 +86,7 @@ void		PrintVariables(VariableSpace space);
 bool		SetVariable(VariableSpace space, const char *name, const char *value);
 bool		SetVariableBool(VariableSpace space, const char *name);
 bool		DeleteVariable(VariableSpace space, const char *name);
+bool		SetEnvVariable(const char *cmd, const char *name, const char *value);
 
 void		SetVariableHooks(VariableSpace space, const char *name,
 							 VariableSubstituteHook shook,
diff --git src/test/regress/expected/psql.out src/test/regress/expected/psql.out
index 7204fdb0b4..82e8f435b1 100644
--- src/test/regress/expected/psql.out
+++ src/test/regress/expected/psql.out
@@ -152,6 +152,45 @@ more than one row returned for \gset
 select 10 as test01, 20 as test02 from generate_series(1,0) \gset
 no rows returned for \gset
 \unset FETCH_COUNT
+-- \gsetenv
+select 10 as test01, 20 as test02, 'Hello' as test03 \gsetenv pref01_
+\! echo $pref01_test01 $pref01_test02 $pref01_test03
+10 20 Hello
+-- should fail: bad environment variable name
+select 10 as "bad=name"
+\gsetenv
+\gsetenv: environment variable name must not contain "="
+-- multiple backslash commands in one line
+select 1 as x, 2 as y \gsetenv pref01_ \\ \! echo $pref01_x
+1
+select 3 as x, 4 as y \gsetenv pref01_ \\ \g \! echo $pref01_x $pref01_y
+ x | y 
+---+---
+ 3 | 4
+(1 row)
+
+3 4
+-- NULL should unset the variable
+\setenv var2 xyz
+select 1 as var1, NULL as var2, 3 as var3 \gsetenv
+\! echo $var1 $var2 $var3
+1 3
+-- \gsetenv requires just one tuple
+select 10 as test01, 20 as test02 from generate_series(1,3) \gsetenv
+more than one row returned for \gsetenv
+select 10 as test01, 20 as test02 from generate_series(1,0) \gsetenv
+no rows returned for \gsetenv
+-- \gsetenv should work in FETCH_COUNT mode too
+\set FETCH_COUNT 1
+select 1 as x, 2 as y \gsetenv pref01_ \\ \! echo $pref01_x
+1
+select 3 as x, 4 as y \gsetenv pref01_ \! echo $pref01_x $pref01_y
+3 4
+select 10 as test01, 20 as test02 from generate_series(1,3) \gsetenv
+more than one row returned for \gsetenv
+select 10 as test01, 20 as test02 from generate_series(1,0) \gsetenv
+no rows returned for \gsetenv
+\unset FETCH_COUNT
 -- \gdesc
 SELECT
     NULL AS zero,
diff --git src/test/regress/sql/psql.sql src/test/regress/sql/psql.sql
index 537d5332aa..a7e9ac77b3 100644
--- src/test/regress/sql/psql.sql
+++ src/test/regress/sql/psql.sql
@@ -83,6 +83,39 @@ select 10 as test01, 20 as test02 from generate_series(1,0) \gset
 
 \unset FETCH_COUNT
 
+-- \gsetenv
+
+select 10 as test01, 20 as test02, 'Hello' as test03 \gsetenv pref01_
+
+\! echo $pref01_test01 $pref01_test02 $pref01_test03
+
+-- should fail: bad environment variable name
+select 10 as "bad=name"
+\gsetenv
+
+-- multiple backslash commands in one line
+select 1 as x, 2 as y \gsetenv pref01_ \\ \! echo $pref01_x
+select 3 as x, 4 as y \gsetenv pref01_ \\ \g \! echo $pref01_x $pref01_y
+
+-- NULL should unset the variable
+\setenv var2 xyz
+select 1 as var1, NULL as var2, 3 as var3 \gsetenv
+\! echo $var1 $var2 $var3
+
+-- \gsetenv requires just one tuple
+select 10 as test01, 20 as test02 from generate_series(1,3) \gsetenv
+select 10 as test01, 20 as test02 from generate_series(1,0) \gsetenv
+
+-- \gsetenv should work in FETCH_COUNT mode too
+\set FETCH_COUNT 1
+
+select 1 as x, 2 as y \gsetenv pref01_ \\ \! echo $pref01_x
+select 3 as x, 4 as y \gsetenv pref01_ \! echo $pref01_x $pref01_y
+select 10 as test01, 20 as test02 from generate_series(1,3) \gsetenv
+select 10 as test01, 20 as test02 from generate_series(1,0) \gsetenv
+
+\unset FETCH_COUNT
+
 -- \gdesc
 
 SELECT

--------------2.28.0--


#2Tom Lane
tgl@sss.pgh.pa.us
In reply to: David Fetter (#1)
Re: \gsetenv

David Fetter <david@fetter.org> writes:

We have \gset to set some parameters, but not ones in the environment,
so I fixed this with a new analogous command, \gsetenv.

In view of the security complaints we just had about \gset
(CVE-2020-25696), I cannot fathom why we'd consider adding another
way to cause similar problems.

We were fortunate enough to be able to close off the main security risk
of \gset without deleting the feature altogether ... but how exactly
would we distinguish "safe" from "unsafe" environment variables? It kind
of seems like anything that would be worth setting at all would tend to
fall into the "unsafe" category, because the implications of setting it
would be unclear. But *for certain* we're not taking a patch that allows
remotely setting PATH and things like that.

Besides which, you haven't bothered with even one word of positive
justification. What's the non-hazardous use case?

regards, tom lane

#3David Fetter
david@fetter.org
In reply to: Tom Lane (#2)
Re: \gsetenv

On Wed, Dec 16, 2020 at 05:30:13PM -0500, Tom Lane wrote:

David Fetter <david@fetter.org> writes:

We have \gset to set some parameters, but not ones in the environment,
so I fixed this with a new analogous command, \gsetenv.

In view of the security complaints we just had about \gset
(CVE-2020-25696), I cannot fathom why we'd consider adding another
way to cause similar problems.

The RedHat site says, in part:

the attacker can execute arbitrary code as the operating system
account running psql

This is true of literally everything that requires a login shell in
order to use. I remember a "virus" my friend Keith McMillan wrote in
TeX back in the 1994. You can download the PostScript file detailing
the procedure, bearing in mind that PostScript also contains ways to
execute arbitrary code if opened:

ftp://ftp.cerias.purdue.edu/pub/doc/viruses/KeithMcMillan-PlatformIndependantVirus.ps

That one got itself a remote code execution by fiddling with a
person's .emacs, and it got Keith a master's degree in CS. I suspect
that equally nasty things are possible when it comes to \i and \o in
psql. It would be a terrible idea to hobble psql in the attempt to
prevent such attacks.

We were fortunate enough to be able to close off the main security
risk of \gset without deleting the feature altogether ... but how
exactly would we distinguish "safe" from "unsafe" environment
variables? It kind of seems like anything that would be worth
setting at all would tend to fall into the "unsafe" category,
because the implications of setting it would be unclear. But *for
certain* we're not taking a patch that allows remotely setting PATH
and things like that.

Would you be so kind as to explain what the actual problem is here
that not doing this would mitigate?

If people run code they haven't seen from a server they don't trust,
neither psql nor anything else[1]search for "gods themselves" here: https://en.wikiquote.org/wiki/Friedrich_Schiller -- David Fetter <david(at)fetter(dot)org> http://fetter.org/ Phone: +1 415 235 3778 can protect them from the
consequences. Seeing what they're about to run is dead easy in this
case because \gsetenv, like \gset and what in my view is the much more
dangerous \gexec, is something anyone with the tiniest modicum of
caution would run only after testing it with \g.

Besides which, you haven't bothered with even one word of positive
justification. What's the non-hazardous use case?

Thanks for asking, and my apologies for not including it.

I ran into a situation where we sometimes got a very heavily loaded
and also well-protected PostgreSQL server. At times, just getting a
shell on it could take a few tries. To mitigate situations like that,
I used a method that's a long way from new, abstruse, or secret: have
psql open in a long-lasting tmux or screen session. It could both
access the database at a time when getting a new connection would be
somewhere between difficult and impossible. The bit that's unlikely
to be new was when I noticed that it could also shell out
and send information off to other places, but only when I put together
a pretty baroque procedure that involved using combinations of \gset,
\o, and \!. All of the same things \gsetenv could do were doable with
those, just less convenient, so I drafted up a patch in the hope that
fewer others would find themselves jumping through the hoops I did to
get that set up.

Separately, I confess to some bafflement at the reasoning behind the
CVE you referenced. By the time an attacker has compromised a database
server, it's already game over. Code running on the compromised
database is capable of doing much nastier things than crashing a
client machine, and very likely has access to other high-value targets
on its own say-so than said client does.

Best,
David.

[1]: search for "gods themselves" here: https://en.wikiquote.org/wiki/Friedrich_Schiller -- David Fetter <david(at)fetter(dot)org> http://fetter.org/ Phone: +1 415 235 3778
https://en.wikiquote.org/wiki/Friedrich_Schiller
--
David Fetter <david(at)fetter(dot)org> http://fetter.org/
Phone: +1 415 235 3778

Remember to vote!
Consider donating to Postgres: http://www.postgresql.org/about/donate

#4Andrew Dunstan
andrew@dunslane.net
In reply to: David Fetter (#3)
Re: \gsetenv

On 12/16/20 10:54 PM, David Fetter wrote:

Besides which, you haven't bothered with even one word of positive
justification. What's the non-hazardous use case?

Thanks for asking, and my apologies for not including it.

I ran into a situation where we sometimes got a very heavily loaded
and also well-protected PostgreSQL server. At times, just getting a
shell on it could take a few tries. To mitigate situations like that,
I used a method that's a long way from new, abstruse, or secret: have
psql open in a long-lasting tmux or screen session. It could both
access the database at a time when getting a new connection would be
somewhere between difficult and impossible. The bit that's unlikely
to be new was when I noticed that it could also shell out
and send information off to other places, but only when I put together
a pretty baroque procedure that involved using combinations of \gset,
\o, and \!. All of the same things \gsetenv could do were doable with
those, just less convenient, so I drafted up a patch in the hope that
fewer others would find themselves jumping through the hoops I did to
get that set up.

Does this help?

andrew=# select 'abc'::text as foo \gset
andrew=# \setenv FOO :foo
andrew=# \! echo $FOO
abc
andrew=#

cheers

andrew

--
Andrew Dunstan
EDB: https://www.enterprisedb.com

#5Fabien COELHO
coelho@cri.ensmp.fr
In reply to: David Fetter (#1)
Re: \gsetenv

Hello David,

We have \gset to set some parameters, but not ones in the environment,
so I fixed this with a new analogous command, \gsetenv. I considered
refactoring SetVariable to include environment variables, but for a
first cut, I just made a separate function and an extra if.

My 0.02ᅵ: ISTM that you do not really need that, it can already be
achieved with gset, so I would not bother to add a gsetenv.

sh> psql
SELECT 'Calvin' AS foo \gset
\setenv FOO :foo
\! echo $FOO
Calvin

--
Fabien.

#6David Fetter
david@fetter.org
In reply to: Fabien COELHO (#5)
Re: \gsetenv

On Sun, Dec 20, 2020 at 02:26:14PM +0100, Fabien COELHO wrote:

Hello David,

We have \gset to set some parameters, but not ones in the environment,
so I fixed this with a new analogous command, \gsetenv. I considered
refactoring SetVariable to include environment variables, but for a
first cut, I just made a separate function and an extra if.

My 0.02€: ISTM that you do not really need that, it can already be achieved
with gset, so I would not bother to add a gsetenv.

sh> psql
SELECT 'Calvin' AS foo \gset
\setenv FOO :foo
\! echo $FOO
Calvin

Thanks!

You're the second person who's mentioned this workaround, which goes
to a couple of points I tried to make earlier:

- This is not by any means a new capability, just a convenience, and
- In view of the fact that it's a very old capability, the idea that
it has implications for controlling access or other parts of the
space of threat models is pretty silly.

Having dispensed with the idea that there's a new attack surface here,
I'd like to request that people at least have a look at it as a
feature psql users might appreciate having. As the author, I obviously
see it that way, but again as the author, it's not for me to make that
call.

Best,
David.
--
David Fetter <david(at)fetter(dot)org> http://fetter.org/
Phone: +1 415 235 3778

Remember to vote!
Consider donating to Postgres: http://www.postgresql.org/about/donate

#7Tom Lane
tgl@sss.pgh.pa.us
In reply to: David Fetter (#6)
Re: \gsetenv

David Fetter <david@fetter.org> writes:

On Sun, Dec 20, 2020 at 02:26:14PM +0100, Fabien COELHO wrote:

SELECT 'Calvin' AS foo \gset
\setenv FOO :foo
\! echo $FOO
Calvin

You're the second person who's mentioned this workaround, which goes
to a couple of points I tried to make earlier:

- This is not by any means a new capability, just a convenience, and
- In view of the fact that it's a very old capability, the idea that
it has implications for controlling access or other parts of the
space of threat models is pretty silly.

This is essentially the same workaround as what we recommend for anyone
who's unhappy with the fix for CVE-2020-25696: do \gset into a non-special
variable and then copy to the special variable. The reason it seems okay
is that now it is clear that client-side logic intends the special
variable change to happen. Thus a compromised server cannot hijack your
client-side session all by itself. There's nonzero risk in letting the
server modify your PROMPT1, PATH, or whatever, but you took the risk
intentionally (and, presumably, it's necessary to your purposes).

I tend to agree with you that the compromised-server argument is a little
bit of a stretch. Still, we did have an actual user complaining about
the case for \gset, and it's clear that in at least some scenarios this
sort of attack could be used to parlay a server compromise into additional
access. So we're not likely to undo the CVE-2020-25696 fix, and we're
equally unlikely to provide an unrestricted way to set environment
variables directly from the server.

If we could draw a line between "safe" and "unsafe" environment
variables, I'd be willing to consider a patch that allows directly
setting only the former. But I don't see how to draw that line.
Most of the point of any such feature would have to be to affect
the behavior of shell commands subsequently invoked with \! ...
and we can't know what a given variable would do to those. So on
the whole I'm inclined to leave things as-is, where people have to
do the assignment manually.

regards, tom lane

#8David Fetter
david@fetter.org
In reply to: Tom Lane (#7)
Re: \gsetenv

On Sun, Dec 20, 2020 at 01:07:12PM -0500, Tom Lane wrote:

David Fetter <david@fetter.org> writes:

On Sun, Dec 20, 2020 at 02:26:14PM +0100, Fabien COELHO wrote:

SELECT 'Calvin' AS foo \gset
\setenv FOO :foo
\! echo $FOO
Calvin

You're the second person who's mentioned this workaround, which goes
to a couple of points I tried to make earlier:

- This is not by any means a new capability, just a convenience, and
- In view of the fact that it's a very old capability, the idea that
it has implications for controlling access or other parts of the
space of threat models is pretty silly.

This is essentially the same workaround as what we recommend for anyone
who's unhappy with the fix for CVE-2020-25696: do \gset into a non-special
variable and then copy to the special variable. The reason it seems okay
is that now it is clear that client-side logic intends the special
variable change to happen. Thus a compromised server cannot hijack your
client-side session all by itself. There's nonzero risk in letting the
server modify your PROMPT1, PATH, or whatever, but you took the risk
intentionally (and, presumably, it's necessary to your purposes).

I tend to agree with you that the compromised-server argument is a little
bit of a stretch. Still, we did have an actual user complaining about
the case for \gset, and it's clear that in at least some scenarios this
sort of attack could be used to parlay a server compromise into additional
access. So we're not likely to undo the CVE-2020-25696 fix, and we're
equally unlikely to provide an unrestricted way to set environment
variables directly from the server.

If we could draw a line between "safe" and "unsafe" environment
variables, I'd be willing to consider a patch that allows directly
setting only the former. But I don't see how to draw that line.
Most of the point of any such feature would have to be to affect
the behavior of shell commands subsequently invoked with \! ...
and we can't know what a given variable would do to those. So on
the whole I'm inclined to leave things as-is, where people have to
do the assignment manually.

I suppose now's not a great time for this from an optics point of
view. Taking on the entire security theater industry is way out of
scope for the PostgreSQL project.

We have plenty of ways to spawn shells and cause havoc, and we
wouldn't be able to block them all even if we decided to put a bunch
of pretty onerous restrictions on psql at this very late date. We have
\set, backticks, \!, and bunches of things less obvious that could,
even without a compromised server, cause real mischief. I believe that
a more effective way to deal with this reality in a way that helps
users is to put clear warnings in the documentation about the fact
that psql programs are at least as capable as shell programs in that
they are innately capable of doing anything that the psql's system
user is authorized to do.

Would a patch along that line help?

Best,
David.
--
David Fetter <david(at)fetter(dot)org> http://fetter.org/
Phone: +1 415 235 3778

Remember to vote!
Consider donating to Postgres: http://www.postgresql.org/about/donate

#9Heikki Linnakangas
hlinnaka@iki.fi
In reply to: David Fetter (#8)
Re: \gsetenv

On 20/12/2020 21:05, David Fetter wrote:

We have plenty of ways to spawn shells and cause havoc, and we
wouldn't be able to block them all even if we decided to put a bunch
of pretty onerous restrictions on psql at this very late date. We have
\set, backticks, \!, and bunches of things less obvious that could,
even without a compromised server, cause real mischief.

There is a big difference between having to trust the server or not.
Yeah, you could cause a lot of mischief if you let a user run arbitrary
psql scripts on your behalf. But that's no excuse for opening up a whole
another class of problems.

- Heikki

#10David Fetter
david@fetter.org
In reply to: Heikki Linnakangas (#9)
Re: \gsetenv

On Sun, Dec 20, 2020 at 10:42:40PM +0200, Heikki Linnakangas wrote:

On 20/12/2020 21:05, David Fetter wrote:

We have plenty of ways to spawn shells and cause havoc, and we
wouldn't be able to block them all even if we decided to put a bunch
of pretty onerous restrictions on psql at this very late date. We have
\set, backticks, \!, and bunches of things less obvious that could,
even without a compromised server, cause real mischief.

There is a big difference between having to trust the server or not. Yeah,
you could cause a lot of mischief if you let a user run arbitrary psql
scripts on your behalf. But that's no excuse for opening up a whole another
class of problems.

I'm skittish about putting exploits out in public in advance of
discussions about how to mitigate them, but I have constructed several
that do pretty bad things using only hostile content in a server and
the facilities `psql` already provides.

Best,
David.
--
David Fetter <david(at)fetter(dot)org> http://fetter.org/
Phone: +1 415 235 3778

Remember to vote!
Consider donating to Postgres: http://www.postgresql.org/about/donate

#11David G. Johnston
david.g.johnston@gmail.com
In reply to: Tom Lane (#7)
Re: \gsetenv

On Sun, Dec 20, 2020 at 11:07 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:

If we could draw a line between "safe" and "unsafe" environment
variables, I'd be willing to consider a patch that allows directly
setting only the former. But I don't see how to draw that line.

IIUC the threat here is for users that write:

SELECT * FROM view \gset

Because if you are writing

SELECT col1, col2, col3 OR SELECT expression AS col1 \gset

The query author has explicitly stated which variable names they exactly
want to change/create and nothing the server can do will be able to alter
those names.

Or *is* that the problem - the server might decide to send back a column
named "breakme1" in the first column position even though the user aliased
the column name as "col1"?

Would a "\gsetenv (col1, col2, col3, skip, col4)" be acceptable that leaves
the name locally defined while relying on column position to match?

How much do we want to handicap a useful feature because someone can use it
alongside "SELECT *"? Can we prevent it from working in such a case
outright - force an explicit column name list at minimum, and ideally
aliases for expressions? I suspect not, too much of that has to happen on
the server. That makes doing this by column position and defining the
names strictly locally a compromise worth considering. At worst, there is
no way to get an unwanted variable to appear on the client even if the data
for wanted variables is made bogus by the compromised server. I don't see
how avoiding the bogus data problem is even possible.

David J.

#12Tom Lane
tgl@sss.pgh.pa.us
In reply to: David G. Johnston (#11)
Re: \gsetenv

"David G. Johnston" <david.g.johnston@gmail.com> writes:

On Sun, Dec 20, 2020 at 11:07 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:

If we could draw a line between "safe" and "unsafe" environment
variables, I'd be willing to consider a patch that allows directly
setting only the former. But I don't see how to draw that line.

Because if you are writing
SELECT col1, col2, col3 OR SELECT expression AS col1 \gset
The query author has explicitly stated which variable names they exactly
want to change/create and nothing the server can do will be able to alter
those names.

Or *is* that the problem - the server might decide to send back a column
named "breakme1" in the first column position even though the user aliased
the column name as "col1"?

Yeah, exactly. Just because the SQL output *should* have column names
x, y, z doesn't mean it *will*, if the server is malicious. psql isn't
bright enough to understand what column names the query ought to produce,
so it just believes the column names that come back in the query result.

Would a "\gsetenv (col1, col2, col3, skip, col4)" be acceptable that leaves
the name locally defined while relying on column position to match?

Hmm, maybe. The key point here is local vs. remote control of which
variables get assigned to, and offhand that seems like it'd fix the
problem.

How much do we want to handicap a useful feature because someone can use it
alongside "SELECT *"?

Whether it's "SELECT *" or "SELECT 1 AS X" doesn't really matter here.
The concern is that somebody has hacked the server to send back something
that is *not* what you asked for. For that matter, since the actual
update isn't visible to the user, the attacker could easily make the
server send back all the columns the user expected ... plus some
he didn't. The attackee might not even realize till later that
something fishy happened.

regards, tom lane