Skip to content

Commit e428cd0

Browse files
committed
Block environment variable mutations from trusted PL/Perl.
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
1 parent 706a96c commit e428cd0

File tree

6 files changed

+164
-2
lines changed

6 files changed

+164
-2
lines changed

doc/src/sgml/plperl.sgml

+13
Original file line numberDiff line numberDiff line change
@@ -1093,6 +1093,19 @@ $$ LANGUAGE plperl;
10931093
be permitted to use this language.
10941094
</para>
10951095

1096+
<warning>
1097+
<para>
1098+
Trusted PL/Perl relies on the Perl <literal>Opcode</literal> module to
1099+
preserve security.
1100+
Perl
1101+
<ulink url="https://perldoc.perl.org/Opcode#WARNING">documents</ulink>
1102+
that the module is not effective for the trusted PL/Perl use case. If
1103+
your security needs are incompatible with the uncertainty in that warning,
1104+
consider executing <literal>REVOKE USAGE ON LANGUAGE plperl FROM
1105+
PUBLIC</literal>.
1106+
</para>
1107+
</warning>
1108+
10961109
<para>
10971110
Here is an example of a function that will not work because file
10981111
system operations are not allowed for security reasons:

src/pl/plperl/GNUmakefile

+2-2
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,10 @@ endif # win32
5555

5656
SHLIB_LINK = $(perl_embed_ldflags)
5757

58-
REGRESS_OPTS = --dbname=$(PL_TESTDB)
58+
REGRESS_OPTS = --dbname=$(PL_TESTDB) --dlpath=$(top_builddir)/src/test/regress
5959
REGRESS = plperl_setup plperl plperl_lc plperl_trigger plperl_shared \
6060
plperl_elog plperl_util plperl_init plperlu plperl_array \
61-
plperl_call plperl_transaction
61+
plperl_call plperl_transaction plperl_env
6262
# if Perl can support two interpreters in one backend,
6363
# test plperl-and-plperlu cases
6464
ifneq ($(PERL),)

src/pl/plperl/input/plperl_env.source

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
--
2+
-- Test the environment setting
3+
--
4+
5+
CREATE FUNCTION get_environ()
6+
RETURNS text[]
7+
AS '@libdir@/regress@DLSUFFIX@', 'get_environ'
8+
LANGUAGE C STRICT;
9+
10+
-- fetch the process environment
11+
12+
CREATE FUNCTION process_env () RETURNS text[]
13+
LANGUAGE plpgsql AS
14+
$$
15+
16+
declare
17+
res text[];
18+
tmp text[];
19+
f record;
20+
begin
21+
for f in select unnest(get_environ()) as t loop
22+
tmp := regexp_split_to_array(f.t, '=');
23+
if array_length(tmp, 1) = 2 then
24+
res := res || tmp;
25+
end if;
26+
end loop;
27+
return res;
28+
end
29+
30+
$$;
31+
32+
-- plperl should not be able to affect the process environment
33+
34+
DO
35+
$$
36+
$ENV{TEST_PLPERL_ENV_FOO} = "shouldfail";
37+
untie %ENV;
38+
$ENV{TEST_PLPERL_ENV_FOO} = "testval";
39+
my $penv = spi_exec_query("select unnest(process_env()) as pe");
40+
my %received;
41+
for (my $f = 0; $f < $penv->{processed}; $f += 2)
42+
{
43+
my $k = $penv->{rows}[$f]->{pe};
44+
my $v = $penv->{rows}[$f+1]->{pe};
45+
$received{$k} = $v;
46+
}
47+
unless (exists $received{TEST_PLPERL_ENV_FOO})
48+
{
49+
elog(NOTICE, "environ unaffected")
50+
}
51+
52+
$$ LANGUAGE plperl;
+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
--
2+
-- Test the environment setting
3+
--
4+
CREATE FUNCTION get_environ()
5+
RETURNS text[]
6+
AS '@libdir@/regress@DLSUFFIX@', 'get_environ'
7+
LANGUAGE C STRICT;
8+
-- fetch the process environment
9+
CREATE FUNCTION process_env () RETURNS text[]
10+
LANGUAGE plpgsql AS
11+
$$
12+
13+
declare
14+
res text[];
15+
tmp text[];
16+
f record;
17+
begin
18+
for f in select unnest(get_environ()) as t loop
19+
tmp := regexp_split_to_array(f.t, '=');
20+
if array_length(tmp, 1) = 2 then
21+
res := res || tmp;
22+
end if;
23+
end loop;
24+
return res;
25+
end
26+
27+
$$;
28+
-- plperl should not be able to affect the process environment
29+
DO
30+
$$
31+
$ENV{TEST_PLPERL_ENV_FOO} = "shouldfail";
32+
untie %ENV;
33+
$ENV{TEST_PLPERL_ENV_FOO} = "testval";
34+
my $penv = spi_exec_query("select unnest(process_env()) as pe");
35+
my %received;
36+
for (my $f = 0; $f < $penv->{processed}; $f += 2)
37+
{
38+
my $k = $penv->{rows}[$f]->{pe};
39+
my $v = $penv->{rows}[$f+1]->{pe};
40+
$received{$k} = $v;
41+
}
42+
unless (exists $received{TEST_PLPERL_ENV_FOO})
43+
{
44+
elog(NOTICE, "environ unaffected")
45+
}
46+
47+
$$ LANGUAGE plperl;
48+
WARNING: attempted alteration of $ENV{TEST_PLPERL_ENV_FOO} at line 12.
49+
NOTICE: environ unaffected

src/pl/plperl/plc_trusted.pl

+24
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,27 @@ package PostgreSQL::InServer::safe; ## no critic (RequireFilenameMatchesPackage)
2727
require Carp::Heavy;
2828
require warnings;
2929
require feature if $] >= 5.010000;
30+
31+
#<<< protect next line from perltidy so perlcritic annotation works
32+
package PostgreSQL::InServer::WarnEnv; ## no critic (RequireFilenameMatchesPackage)
33+
#>>>
34+
35+
use strict;
36+
use warnings;
37+
use Tie::Hash;
38+
our @ISA = qw(Tie::StdHash);
39+
40+
sub STORE { warn "attempted alteration of \$ENV{$_[1]}"; }
41+
sub DELETE { warn "attempted deletion of \$ENV{$_[1]}"; }
42+
sub CLEAR { warn "attempted clearance of ENV hash"; }
43+
44+
# Remove magic property of %ENV. Changes to this will now not be reflected in
45+
# the process environment.
46+
*main::ENV = {%ENV};
47+
48+
# Block %ENV changes from trusted PL/Perl, and warn. We changed %ENV to just a
49+
# normal hash, yet the application may be expecting the usual Perl %ENV
50+
# magic. Blocking and warning avoids silent application breakage. The user can
51+
# untie or otherwise disable this, e.g. if the lost mutation is unimportant
52+
# and modifying the code to stop that mutation would be onerous.
53+
tie %main::ENV, 'PostgreSQL::InServer::WarnEnv', %ENV or die $!;

src/test/regress/regress.c

+24
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
#include "optimizer/plancat.h"
3636
#include "port/atomics.h"
3737
#include "storage/spin.h"
38+
#include "utils/array.h"
3839
#include "utils/builtins.h"
3940
#include "utils/geo_decls.h"
4041
#include "utils/memutils.h"
@@ -624,6 +625,29 @@ make_tuple_indirect(PG_FUNCTION_ARGS)
624625
PG_RETURN_POINTER(newtup->t_data);
625626
}
626627

628+
PG_FUNCTION_INFO_V1(get_environ);
629+
630+
Datum
631+
get_environ(PG_FUNCTION_ARGS)
632+
{
633+
extern char **environ;
634+
int nvals = 0;
635+
ArrayType *result;
636+
Datum *env;
637+
638+
for (char **s = environ; *s; s++)
639+
nvals++;
640+
641+
env = palloc(nvals * sizeof(Datum));
642+
643+
for (int i = 0; i < nvals; i++)
644+
env[i] = CStringGetTextDatum(environ[i]);
645+
646+
result = construct_array(env, nvals, TEXTOID, -1, false, TYPALIGN_INT);
647+
648+
PG_RETURN_POINTER(result);
649+
}
650+
627651
PG_FUNCTION_INFO_V1(regress_putenv);
628652

629653
Datum

0 commit comments

Comments
 (0)