From 16917439daab97ddb0a58a7b75d17d47c60535fe Mon Sep 17 00:00:00 2001
From: Kosta Harlan <kharlan@wikimedia.org>
Date: Mon, 18 May 2026 20:02:32 +0200
Subject: [PATCH 2/2] SECURITY: EasyTimeline: Strip newlines from BarData and
 Scale text

Why:
- Follow-up to T426631. The original strip only covered TextData,
  but ParseBarData and ParseScale have the same shape and were
  missed: real LFs introduced by ExtractText survive into the
  generated ploticus script as column-0 directives. -noshell
  blocks the shell-exec sink today, but the directive-injection
  primitive is still attacker-controlled
- The TextData strip regex did not cover bare CR or other
  vertical whitespace either

What:
- In ParseBarData, strip vertical whitespace and the literal \n
  sequence from the Text= attribute, mapping both to '~'. Closes
  injection into the 'stubs: text' block written by WritePlotFile
- In ParseScale, do the same on the Text= attribute before the
  existing '~ -> \n' transform. Closes injection into the
  'stubs: list' arg written by PlotScale
- Widen the existing ParseTextData strip from /(?:\n|\\n)/ to
  /(?:\v|\\n)/ so bare CR and other vertical whitespace are
  neutralised consistently with the new sites

Assisted-by: Claude Opus 4.7
Bug: T426631
---
 scripts/EasyTimeline.pl | 27 +++++++++++++++++++--------
 1 file changed, 19 insertions(+), 8 deletions(-)

diff --git a/scripts/EasyTimeline.pl b/scripts/EasyTimeline.pl
index a865620..048da11 100755
--- a/scripts/EasyTimeline.pl
+++ b/scripts/EasyTimeline.pl
@@ -817,7 +817,13 @@ sub ParseBarData {
             # }
             elsif ($attribute =~ /^Text$/i) {
                 $text = $attrvalue;
-                $text =~ s/\\n/~/gs;
+                # Strip newline-ish content that would otherwise close
+                # out the BarData label and reach the 'stubs: text'
+                # block of the generated ploticus script as a column-0
+                # directive. \v covers LF, CR and other vertical
+                # whitespace; \\n catches the literal two-char form
+                # that ParseText derives from '~'.
+                $text =~ s/(?:\v|\\n)/~/gs;
                 if ($text =~ /\~/) {
                     &Warning( "BarData attribute 'text' contains ~ (tilde).\n"
                             . "Tilde will not be translated into newline character (only in PlotData)"
@@ -2519,6 +2525,10 @@ sub ParseScale {
             delete($Attributes{"grid"});
         }
         elsif ($attribute =~ /Text/i) {
+            # Strip newline-ish content that would otherwise close out
+            # the 'stubs: list' arg in PlotScale and reach the
+            # generated ploticus script as a column-0 directive.
+            $attrvalue =~ s/(?:\v|\\n)/~/gs;
             $attrvalue =~ s/\~/\\n/g;
             $attrvalue =~ s/^\"//g;
             $attrvalue =~ s/\"$//g;
@@ -2727,13 +2737,14 @@ sub ParseTextData {
                 # 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;
+                # Two forms can arrive here: real vertical whitespace
+                # (LF / CR / VT / FF / etc.) 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/(?:\v|\\n)/~/gs;
                 if ($text =~ /\~/) {
                     &Warning("TextData attribute 'text' contains ~ (tilde).\n"
                             . "Tilde will not be translated into newline character (only in PlotData)"
-- 
2.50.1 (Apple Git-155)

