Block environment variable mutations from trusted PL/Perl.
authorNoah Misch <[email protected]>
Mon, 11 Nov 2024 14:23:43 +0000 (06:23 -0800)
committerNoah Misch <[email protected]>
Mon, 11 Nov 2024 14:23:48 +0000 (06:23 -0800)
Many process environment variables (e.g. PATH), bypass the containment
expected of a trusted PL.  Hence, trusted PLs must not offer features
that achieve setenv().  Otherwise, an attacker having USAGE privilege on
the language often can achieve arbitrary code execution, even if the
attacker lacks a database server operating system user.

To fix PL/Perl, replace trusted PL/Perl %ENV with a tied hash that just
replaces each modification attempt with a warning.  Sites that reach
these warnings should evaluate the application-specific implications of
proceeding without the environment modification:

  Can the application reasonably proceed without the modification?

    If no, switch to plperlu or another approach.

    If yes, the application should change the code to stop attempting
    environment modifications.  If that's too difficult, add "untie
    %main::ENV" in any code executed before the warning.  For example,
    one might add it to the start of the affected function or even to
    the plperl.on_plperl_init setting.

In passing, link to Perl's guidance about the Perl features behind the
security posture of PL/Perl.

Back-patch to v12 (all supported versions).

Andrew Dunstan and Noah Misch

Security: CVE-2024-10979

doc/src/sgml/plperl.sgml
src/pl/plperl/GNUmakefile
src/pl/plperl/input/plperl_env.source [new file with mode: 0644]
src/pl/plperl/output/plperl_env.source [new file with mode: 0644]
src/pl/plperl/plc_trusted.pl
src/test/regress/regress.c

index bf47fd30d616c8ed4ca77426059de2ad37b51a68..e20af2dfaed768699fc8072682a97742ea745842 100644 (file)
@@ -1050,6 +1050,19 @@ $$ LANGUAGE plperl;
    be permitted to use this language.
   </para>
 
+  <warning>
+   <para>
+    Trusted PL/Perl relies on the Perl <literal>Opcode</literal> module to
+    preserve security.
+    Perl
+    <ulink url="https://perldoc.perl.org/Opcode#WARNING">documents</ulink>
+    that the module is not effective for the trusted PL/Perl use case.  If
+    your security needs are incompatible with the uncertainty in that warning,
+    consider executing <literal>REVOKE USAGE ON LANGUAGE plperl FROM
+    PUBLIC</literal>.
+   </para>
+  </warning>
+
   <para>
    Here is an example of a function that will not work because file
    system operations are not allowed for security reasons:
index d67753daf4d6344fbbff8391184795226ff87fb0..a8e2e0f68f521aabe8363dba889ad774348d7cc5 100644 (file)
@@ -55,8 +55,8 @@ endif # win32
 
 SHLIB_LINK = $(perl_embed_ldflags)
 
-REGRESS_OPTS = --dbname=$(PL_TESTDB) --load-extension=plperl  --load-extension=plperlu
-REGRESS = plperl plperl_lc plperl_trigger plperl_shared plperl_elog plperl_util plperl_init plperlu plperl_array plperl_call plperl_transaction
+REGRESS_OPTS = --dbname=$(PL_TESTDB) --dlpath=$(top_builddir)/src/test/regress --load-extension=plperl  --load-extension=plperlu
+REGRESS = plperl plperl_lc plperl_trigger plperl_shared plperl_elog plperl_util plperl_init plperlu plperl_array plperl_call plperl_transaction plperl_env
 # if Perl can support two interpreters in one backend,
 # test plperl-and-plperlu cases
 ifneq ($(PERL),)
diff --git a/src/pl/plperl/input/plperl_env.source b/src/pl/plperl/input/plperl_env.source
new file mode 100644 (file)
index 0000000..8fe526e
--- /dev/null
@@ -0,0 +1,52 @@
+--
+-- Test the environment setting
+--
+
+CREATE FUNCTION get_environ()
+   RETURNS text[]
+   AS '@libdir@/regress@DLSUFFIX@', 'get_environ'
+   LANGUAGE C STRICT;
+
+-- fetch the process environment
+
+CREATE FUNCTION process_env () RETURNS text[]
+LANGUAGE plpgsql AS
+$$
+
+declare
+   res text[];
+   tmp text[];
+   f record;
+begin
+    for f in select unnest(get_environ()) as t loop
+         tmp := regexp_split_to_array(f.t, '=');
+         if array_length(tmp, 1) = 2 then
+            res := res || tmp;
+         end if;
+    end loop;
+    return res;
+end
+
+$$;
+
+-- plperl should not be able to affect the process environment
+
+DO
+$$
+   $ENV{TEST_PLPERL_ENV_FOO} = "shouldfail";
+   untie %ENV;
+   $ENV{TEST_PLPERL_ENV_FOO} = "testval";
+   my $penv = spi_exec_query("select unnest(process_env()) as pe");
+   my %received;
+   for (my $f = 0; $f < $penv->{processed}; $f += 2)
+   {
+      my $k = $penv->{rows}[$f]->{pe};
+      my $v = $penv->{rows}[$f+1]->{pe};
+      $received{$k} = $v;
+   }
+   unless (exists $received{TEST_PLPERL_ENV_FOO})
+   {
+      elog(NOTICE, "environ unaffected")
+   }
+
+$$ LANGUAGE plperl;
diff --git a/src/pl/plperl/output/plperl_env.source b/src/pl/plperl/output/plperl_env.source
new file mode 100644 (file)
index 0000000..37b7e23
--- /dev/null
@@ -0,0 +1,49 @@
+--
+-- Test the environment setting
+--
+CREATE FUNCTION get_environ()
+   RETURNS text[]
+   AS '@libdir@/regress@DLSUFFIX@', 'get_environ'
+   LANGUAGE C STRICT;
+-- fetch the process environment
+CREATE FUNCTION process_env () RETURNS text[]
+LANGUAGE plpgsql AS
+$$
+
+declare
+   res text[];
+   tmp text[];
+   f record;
+begin
+    for f in select unnest(get_environ()) as t loop
+         tmp := regexp_split_to_array(f.t, '=');
+         if array_length(tmp, 1) = 2 then
+            res := res || tmp;
+         end if;
+    end loop;
+    return res;
+end
+
+$$;
+-- plperl should not be able to affect the process environment
+DO
+$$
+   $ENV{TEST_PLPERL_ENV_FOO} = "shouldfail";
+   untie %ENV;
+   $ENV{TEST_PLPERL_ENV_FOO} = "testval";
+   my $penv = spi_exec_query("select unnest(process_env()) as pe");
+   my %received;
+   for (my $f = 0; $f < $penv->{processed}; $f += 2)
+   {
+      my $k = $penv->{rows}[$f]->{pe};
+      my $v = $penv->{rows}[$f+1]->{pe};
+      $received{$k} = $v;
+   }
+   unless (exists $received{TEST_PLPERL_ENV_FOO})
+   {
+      elog(NOTICE, "environ unaffected")
+   }
+
+$$ LANGUAGE plperl;
+WARNING:  attempted alteration of $ENV{TEST_PLPERL_ENV_FOO} at line 12.
+NOTICE:  environ unaffected
index dea3727682cf577fb7b92f2bd9641ac851d282af..5854661fff505dfd2379ba86910fa17b128b9a1d 100644 (file)
@@ -27,3 +27,27 @@ require Carp;
 require Carp::Heavy;
 require warnings;
 require feature if $] >= 5.010000;
+
+#<<< protect next line from perltidy so perlcritic annotation works
+package PostgreSQL::InServer::WarnEnv; ## no critic (RequireFilenameMatchesPackage)
+#>>>
+
+use strict;
+use warnings;
+use Tie::Hash;
+our @ISA = qw(Tie::StdHash);
+
+sub STORE  { warn "attempted alteration of \$ENV{$_[1]}"; }
+sub DELETE { warn "attempted deletion of \$ENV{$_[1]}"; }
+sub CLEAR  { warn "attempted clearance of ENV hash"; }
+
+# Remove magic property of %ENV. Changes to this will now not be reflected in
+# the process environment.
+*main::ENV = {%ENV};
+
+# Block %ENV changes from trusted PL/Perl, and warn. We changed %ENV to just a
+# normal hash, yet the application may be expecting the usual Perl %ENV
+# magic. Blocking and warning avoids silent application breakage. The user can
+# untie or otherwise disable this, e.g. if the lost mutation is unimportant
+# and modifying the code to stop that mutation would be onerous.
+tie %main::ENV, 'PostgreSQL::InServer::WarnEnv', %ENV or die $!;
index 968f8340d9ef175f5fb9b1404dc4f0b3d3082884..381f7af331818b1b6835d11d13599a6c4c323ce8 100644 (file)
@@ -35,6 +35,7 @@
 #include "optimizer/plancat.h"
 #include "port/atomics.h"
 #include "storage/spin.h"
+#include "utils/array.h"
 #include "utils/builtins.h"
 #include "utils/geo_decls.h"
 #include "utils/rel.h"
@@ -625,6 +626,29 @@ make_tuple_indirect(PG_FUNCTION_ARGS)
    PG_RETURN_POINTER(newtup->t_data);
 }
 
+PG_FUNCTION_INFO_V1(get_environ);
+
+Datum
+get_environ(PG_FUNCTION_ARGS)
+{
+   extern char **environ;
+   int         nvals = 0;
+   ArrayType  *result;
+   Datum      *env;
+
+   for (char **s = environ; *s; s++)
+       nvals++;
+
+   env = palloc(nvals * sizeof(Datum));
+
+   for (int i = 0; i < nvals; i++)
+       env[i] = CStringGetTextDatum(environ[i]);
+
+   result = construct_array(env, nvals, TEXTOID, -1, false, 'i');
+
+   PG_RETURN_POINTER(result);
+}
+
 PG_FUNCTION_INFO_V1(regress_putenv);
 
 Datum