Page MenuHomePhabricator

SECURITY-easytimeline-shell-injection.patch

Authored By
kostajh
May 18 2026, 4:38 PM
Size
4 KB
Referenced Files
None
Subscribers
None

SECURITY-easytimeline-shell-injection.patch

From 8c0b4a352ac57de0f698340562ace603c618b6f1 Mon Sep 17 00:00:00 2001
From: Kosta Harlan <kharlan@wikimedia.org>
Date: Mon, 18 May 2026 18:30:44 +0200
Subject: [PATCH] SECURITY: Harden EasyTimeline against script injection via
TextData
Why:
- Embedded newlines in the TextData 'text' attribute could survive
into the generated ploticus script and inject column-0 directives,
which ploticus then handed to /bin/sh via #proc getdata command:.
- The file.err output channel was reflected back to the requester
even when written by something other than EasyTimeline.pl's
Abort() routine.
What:
- Pass -noshell on both ploticus invocations in WritePlotFile to
disable #proc getdata command:, #proc getdata file: wildcard
expansion, and #shell ... #endshell. EasyTimeline never emits any
of these directives, so legitimate input is unaffected.
- In ParseTextData, strip both real newline characters (introduced
by ExtractText) and the literal two-char \n sequence (introduced
by ParseText) from the Text= attribute value, mapping both to
'~'. This closes the path where newline-ish content survived
WriteText mode "^" and reached the generated ploticus script.
- In Timeline::throwRawException, reflect file.err only when it
starts with the well-known EasyTimeline Abort() envelope marker
("<p>EasyTimeline "). Anything else is logged via the timeline
channel and replaced with a generic timeline-compilererr message.
Assisted-by: Claude Opus 4.7
Bug: T426631
---
includes/Timeline.php | 14 ++++++++++++++
scripts/EasyTimeline.pl | 17 ++++++++++++++++-
2 files changed, 30 insertions(+), 1 deletion(-)
diff --git a/includes/Timeline.php b/includes/Timeline.php
index 6e73413..5a8312c 100644
--- a/includes/Timeline.php
+++ b/includes/Timeline.php
@@ -343,11 +343,25 @@ class Timeline implements ParserFirstCallInitHook {
/**
* Cleanup and throw errors from EasyTimeline.pl
*
+ * Only the well-formed error envelope emitted by EasyTimeline.pl's
+ * Abort() routine is reflected back to the requester. Anything else
+ * in file.err is treated as attacker-influenced (e.g. via a future
+ * ploticus injection bug) and is logged server-side only, with a
+ * generic error surfaced to the user.
+ *
* @param string $err
* @throws TimelineException
* @return never
*/
private static function throwRawException( $err ) {
+ if ( !str_starts_with( $err, '<p>EasyTimeline ' ) ) {
+ LoggerFactory::getInstance( 'timeline' )->warning(
+ 'Unexpected EasyTimeline file.err contents',
+ [ 'error' => $err ]
+ );
+ throw new TimelineException( 'timeline-compilererr', [ '' ] );
+ }
+
// Convert the error from poorly-sanitized HTML to plain text
$err = strtr( $err, [
'</p><p>' => "\n\n",
diff --git a/scripts/EasyTimeline.pl b/scripts/EasyTimeline.pl
index 363f987..a865620 100755
--- a/scripts/EasyTimeline.pl
+++ b/scripts/EasyTimeline.pl
@@ -2724,7 +2724,16 @@ sub ParseTextData {
}
elsif ($attribute =~ /^Text$/i) {
$text = $attrvalue;
- $text =~ s/\\n/~/gs;
+ # Strip newline-ish content that would otherwise
+ # survive WriteText mode "^" (which splits only on
+ # caret) and reach the generated ploticus script.
+ # Two forms can arrive here: a real newline char,
+ # produced by ExtractText from a 'text:"...\n..."'
+ # value, and the literal two-char sequence \n that
+ # ParseText derives from '~'. Both are mapped to '~'
+ # so they cannot close out the ploticus 'text:'
+ # attribute and inject column-0 directives.
+ $text =~ s/(?:\n|\\n)/~/gs;
if ($text =~ /\~/) {
&Warning("TextData attribute 'text' contains ~ (tilde).\n"
. "Tilde will not be translated into newline character (only in PlotData)"
@@ -3985,8 +3994,13 @@ sub WritePlotFile {
print "Running Ploticus to generate svg file $file_vector\n";
my $escaped_font_file = EscapeShellArg($font_file);
+ # -noshell disables ploticus directives that invoke /bin/sh
+ # (#proc getdata command:, #proc getdata file:, #shell ... #endshell).
+ # EasyTimeline never emits any of these, so this is a no-op for
+ # legitimate input but blocks command execution via injection.
my $cmd =
EscapeShellArg($pl)
+ . " -noshell"
. " $map -" . "svg" . " -o "
. EscapeShellArg($file_vector) . " "
. EscapeShellArg($file_script)
@@ -4032,6 +4046,7 @@ sub WritePlotFile {
$cmd =
EscapeShellArg($pl)
+ . " -noshell"
. " $map -"
. $image_file_fmt . " -o "
. EscapeShellArg($file_bitmap) . " "
--
2.54.0

File Metadata

Mime Type
text/x-diff
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
26216046
Default Alt Text
SECURITY-easytimeline-shell-injection.patch (4 KB)

Event Timeline