(a) ossp-eperl is a macro system for embedding Perl code within text documents
for example, a file like
abc
<: print 2*3 :>
def
turns into a perl program similar to
print "abc\n";
print 2*3; print "\n";
print "def\n";
which is then evaluated to produce
abc
6
def
(b) eperl has a C-like preprocessor that has
#include (normal) and #sinclude ("safe") directives
(c) as with the C preprocessor, inclusions are recursive
for a file structure of
a1: #include b1
b1: #include c1
c1: <: system('id') :>
preprocessing a1 yields a file consisting of
<: system('id') :>
that then gets turned into perl program
system('id');
that does what one'd expect
(d) "safe" inclusions are identical, except they remove the begin/end tags
(this notice uses <:/:> but they can differ sometimes)
(e) they do /not/ remove the would-be contents of the tags,
it's effectively a s/<://g s/:>//g operation on the result, so given
a1s: #sinclude b1
preprocessing a1s yields a file consisting of
system('id')
that then gets turned into perl program
print " system('id'); \n";
which also does what one'd expect
(f) files prefixed with "http://" are processed specially by issuing an HTTP GET
so a1/a1s could just as well have said
#include http://hinfo.network/index.html
This is all documented behaviour. It's also quite dangerous.
Debian attempts to patch this seemingly-known bug, saying
eperl (2.2.14-3) unstable; urgency=low
* The #sinclude directive calling an external server could allow
execution of arbitrary code (Bug reported by David Madison on the
ePerl mailing list).
-- Denis Barbier <barbier@debian.org> Sat, 16 Jun 2001 00:29:53 +0200
with
--- eperl-2.2.14.orig/eperl_pp.c
+++ eperl-2.2.14/eperl_pp.c
@@ -278,6 +281,11 @@
cp3 += l1;
else if (strncasecmp(cp3, ePerl_end_delimiter, l2) == 0)
cp3 += l2;
+ else if (strncmp(cp3, "#include", 8) == 0) {
+ /* Replace all occurences of #include by #sinclude */
+ strcpy(cp4, "#sinclude");
+ cp4 += 9;
+ }
else
*cp4++ = *cp3++;
}
full context at
https://git.sr.ht/~nabijaczleweli/ossp-eperl/tree/ePerl_2_2_14/item/eperl_pp.c#L271
but the relevant bit is
/* recursive usage */
if ((cp = ePerl_PP_Process(caName, cppINC, 0 /*mode=file*/)) == NULL)
return NULL;
/* make it secure by removing all begin/end delimiters!! */
if ((cp2 = (char *)malloc(strlen(cp))) == NULL)
return NULL;
l1 = strlen(ePerl_begin_delimiter);
l2 = strlen(ePerl_end_delimiter);
for (cp3 = cp, cp4 = cp2; *cp3 != NUL; ) {
if (strncasecmp(cp3, ePerl_begin_delimiter, l1) == 0)
cp3 += l1;
else if (strncasecmp(cp3, ePerl_end_delimiter, l2) == 0)
cp3 += l2;
else
*cp4++ = *cp3++;
}
It's not necessarily easy to see, but it is obvious once you notice,
that all this does is actually just damage the result,
and doesn't change anything formally:
ePerl_PP_Process() returns a fully-preprocessed result
(#-directives removed and resolved),
and cp2/cp4 are copied to the output verbatim.
I can't seem to find the "ePerl mailing list" but it's been 23 years;
The vulnerability can be triggered with the obvious conclusion that
(g) if you instead do
a2: #sinclude b2
b2: #include c2
c2: <<:: system('id') ::>>
preprocessing a2 yields a file consisting of
<: system('id') :>
that then gets turned into perl program
system('id')
oops!
(tbc the same applies if a2 instead said "#include c2",
but this further illustrates that the Debian patch is meaningless).
The manual recommends using #sinclude for data you don't trust (hence, http://...):
https://srhtcdn.githack.com/~nabijaczleweli/ossp-eperl/blob/13ef21849157bcb160de27c88f39402cd4b5edbc/ossp-eperl.pdf#page=7
This bug wouldn't be anything out of the ordinary:
it's been 23 years since the original report (and 27 since the bug was introduced),
and the only use-case for eperl is wml ‒ no-one is using http includes there ‒
but Debian actually purports to fix this,
so this was first reported to debian-security@ on 2024-09-22.
This behaviour is present in all ePerl versions since 2.2.0 (1997-07-18)
(previous versions didn't have a preprocessor:
https://git.sr.ht/~nabijaczleweli/ossp-eperl/tree/tarballs/item/NEWS#L64
https://git.sr.ht/~nabijaczleweli/ossp-eperl/tree/tarballs/item/ChangeLog#L341-505)
and, thus, AFAICT, every version ever distributed by anyone.
Upcoming ossp-eperl 2.2.15 fixes this with
commit 5971b87 ("Fix #sinclude to /actually/ remove all delimiters. Add test to validate this")
<https://git.sr.ht/~nabijaczleweli/ossp-eperl/commit/5971b873fcfd34e51d8c1605b3a58e006538eb05>
(please cf. https://sr.ht/~nabijaczleweli/ossp for more about the new upstream).
Backports are included below.
bpo-2.2.14.diff is based against 2.2.14
bpo-Debian-2.2.14-24.diff is based against Debian 2.2.14-24
(and the current Fedora patch bundle(?),
since it includes Debian 2.2.14-15.1 wholesale)
The patch against 2.2.14 also fixes an overflow if #sincluded data
contains more #include directives than space is left by the removed delimiters.
Neither patch fixes strncasecmp() always being used,
regardless of current delimiter-casedness configuration,
The patch against Debian doesn't fix the "#include" => "#sinclude" mangling
(it's easy to rip out, but the test also tests it, so rip that out too).
Such is the nature of a backport. At least it reduces the working set.
-- >8 -- bpo-2.2.14.diff -- >8 --
diff --git a/eperl_pp.c b/eperl_pp.c
index c4061ae..90d9db2 100644
--- a/eperl_pp.c
+++ b/eperl_pp.c
@@ -269,21 +269,13 @@ char *ePerl_PP_Process(char *cpInput, char **cppINC, int mode)
return NULL;
/* make it secure by removing all begin/end delimiters!! */
- if ((cp2 = (char *)malloc(strlen(cp))) == NULL)
+ if ((cp = (char *)realloc(cp, strlen(cp)*9/8+1)) == NULL)
return NULL;
- l1 = strlen(ePerl_begin_delimiter);
- l2 = strlen(ePerl_end_delimiter);
- for (cp3 = cp, cp4 = cp2; *cp3 != NUL; ) {
- if (strncasecmp(cp3, ePerl_begin_delimiter, l1) == 0)
- cp3 += l1;
- else if (strncasecmp(cp3, ePerl_end_delimiter, l2) == 0)
- cp3 += l2;
- else
- *cp4++ = *cp3++;
- }
- *cp4 = NUL;
- free(cp);
- cp = cp2;
+#define KILLDEL(delimiter) \
+ while ((cp2 = strcasestr(cp, delimiter))) \
+ memmove(cp2, cp2 + strlen(delimiter), (strlen(cp) + 1) - (cp2 - cp) - strlen(delimiter));
+ KILLDEL(ePerl_begin_delimiter)
+ KILLDEL(ePerl_end_delimiter)
}
else if (strncmp(cp, "#if", 3) == 0) {
/*
diff --git a/t/11_validate_sinclude.t b/t/11_validate_sinclude.t
new file mode 100644
index 0000000..01774a7
--- /dev/null
+++ b/t/11_validate_sinclude.t
@@ -0,0 +1,34 @@
+# SPDX-License-Identifier: 0BSD
+require "TEST.pl";
+TEST::init();
+
+print "1..1\n";
+
+$included2 = TEST::tmpfile(<<"EOF");
+<<::system("id")::>>
+EOF
+
+$included = TEST::tmpfile(<<"EOF");
+<:="A":>
+#include $included2
+<:="B":>
+EOF
+
+$basefile = TEST::tmpfile(<<"EOF");
+a
+#sinclude $included
+b
+EOF
+
+$result = `../eperl -P $basefile`;
+
+$want = <<"EOF";
+a
+="A"
+system("id")
+="B"
+b
+EOF
+print ($want eq $result ? "ok" : "not ok");
+
+TEST::cleanup();
-- >8 -- bpo-Debian-2.2.14-24.diff -- >8 --
diff --git a/eperl_pp.c b/eperl_pp.c
index 644bb30..5928eea 100644
--- a/eperl_pp.c
+++ b/eperl_pp.c
@@ -272,26 +272,17 @@ char *ePerl_PP_Process(char *cpInput, char **cppINC, int mode)
return NULL;
/* make it secure by removing all begin/end delimiters!! */
- if ((cp2 = (char *)malloc(strlen(cp)*9/8)) == NULL)
+ if ((cp = (char *)realloc(cp, strlen(cp)*9/8+1)) == NULL)
return NULL;
- l1 = strlen(ePerl_begin_delimiter);
- l2 = strlen(ePerl_end_delimiter);
- for (cp3 = cp, cp4 = cp2; *cp3 != NUL; ) {
- if (strncasecmp(cp3, ePerl_begin_delimiter, l1) == 0)
- cp3 += l1;
- else if (strncasecmp(cp3, ePerl_end_delimiter, l2) == 0)
- cp3 += l2;
- else if (strncmp(cp3, "#include", 8) == 0) {
- /* Replace all occurences of #include by #sinclude */
- strcpy(cp4, "#sinclude");
- cp4 += 9;
- }
- else
- *cp4++ = *cp3++;
+#define KILLDEL(delimiter) \
+ while ((cp2 = strcasestr(cp, delimiter))) \
+ memmove(cp2, cp2 + strlen(delimiter), (strlen(cp) + 1) - (cp2 - cp) - strlen(delimiter));
+ KILLDEL(ePerl_begin_delimiter)
+ KILLDEL(ePerl_end_delimiter)
+ while ((cp2 = strstr(cp, "#include"))) {
+ memmove(cp2 + sizeof("#sinclude")-1, cp2 + sizeof("#include")-1, (strlen(cp) + 1) - (cp2 - cp) - (sizeof("#include")-1));
+ memcpy(cp2, "#sinclude", sizeof("#sinclude")-1);
}
- *cp4 = NUL;
- free(cp);
- cp = cp2;
}
else if (strncmp(cp, "#if", 3) == 0) {
/*
diff --git a/t/11_validate_sinclude.t b/t/11_validate_sinclude.t
new file mode 100644
index 0000000..1235f99
--- /dev/null
+++ b/t/11_validate_sinclude.t
@@ -0,0 +1,34 @@
+# SPDX-License-Identifier: 0BSD
+require "TEST.pl";
+TEST::init();
+
+print "1..1\n";
+
+$included2 = TEST::tmpfile(<<"EOF");
+<<::system("id#include")::>>
+EOF
+
+$included = TEST::tmpfile(<<"EOF");
+<:="A":>
+#include $included2
+<:="B":>
+EOF
+
+$basefile = TEST::tmpfile(<<"EOF");
+a
+#sinclude $included
+b
+EOF
+
+$result = `../eperl -P $basefile`;
+
+$want = <<"EOF";
+a
+="A"
+system("id#sinclude")
+="B"
+b
+EOF
+print ($want eq $result ? "ok" : "not ok");
+
+TEST::cleanup();