Skip to content

Commit

Permalink
Block environment variable mutations from trusted PL/Perl.
Browse files Browse the repository at this point in the history
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
(cherry picked from commit e530835c6cc5b2dbf330ebe6b0a7fb9f19f5a54c)
  • Loading branch information
nmisch authored and shardgupta committed Dec 6, 2024
1 parent e4000ed commit 713085a
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 2 deletions.
13 changes: 13 additions & 0 deletions doc/src/sgml/plperl.sgml
Original file line number Diff line number Diff line change
Expand Up @@ -1093,6 +1093,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:
Expand Down
4 changes: 2 additions & 2 deletions src/pl/plperl/GNUmakefile
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,10 @@ endif # win32

SHLIB_LINK = $(perl_embed_ldflags)

REGRESS_OPTS = --dbname=$(PL_TESTDB)
REGRESS_OPTS = --dbname=$(PL_TESTDB) --dlpath=$(top_builddir)/src/test/regress
REGRESS = plperl_setup plperl plperl_lc plperl_trigger plperl_shared \
plperl_elog plperl_util plperl_init plperlu plperl_array \
plperl_call plperl_transaction
plperl_call plperl_transaction plperl_env
# if Perl can support two interpreters in one backend,
# test plperl-and-plperlu cases
ifneq ($(PERL),)
Expand Down
53 changes: 53 additions & 0 deletions src/pl/plperl/expected/plperl_env.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
--
-- Test the environment setting
--
-- directory path and dlsuffix are passed to us in environment variables
\getenv libdir PG_LIBDIR
\getenv dlsuffix PG_DLSUFFIX
\set regresslib :libdir '/regress' :dlsuffix
CREATE FUNCTION get_environ()
RETURNS text[]
AS :'regresslib', '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
24 changes: 24 additions & 0 deletions src/pl/plperl/plc_trusted.pl
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,27 @@ package PostgreSQL::InServer::safe; ## no critic (RequireFilenameMatchesPackage)
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 $!;
58 changes: 58 additions & 0 deletions src/pl/plperl/sql/plperl_env.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
--
-- Test the environment setting
--

-- directory path and dlsuffix are passed to us in environment variables
\getenv libdir PG_LIBDIR
\getenv dlsuffix PG_DLSUFFIX

\set regresslib :libdir '/regress' :dlsuffix

CREATE FUNCTION get_environ()
RETURNS text[]
AS :'regresslib', '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;
23 changes: 23 additions & 0 deletions src/test/regress/regress.c
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,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, TYPALIGN_INT);

PG_RETURN_POINTER(result);
}

PG_FUNCTION_INFO_V1(regress_setenv);

Datum
Expand Down

0 comments on commit 713085a

Please sign in to comment.