Introduction
As we know, Moodle is known for being one of the most popular open-source learning management systems (LMS), widely used by educational institutions and organizations worldwide. Its robust feature set, including support for custom plugins and extensibility, makes it a go-to choice for managing online learning. However, like any complex platform, Moodle is not without its security challenges.
In this post, we’ll get into an interesting case involving Moodle’s TeX filter. A feature designed for rendering mathematical expressions and how a patch intended to address a prior vulnerability fell short, leaving the door open for exploitation. By revisiting a previous CVE, we’ll explore how the issue persists and demonstrate how an attacker can exploit this flaw for local file read.
Note: Only Moodle instances with the TeX notation filter enabled and TeX Live installed are affected.
Revisiting a past CVE (CVE-2024-43426)
In CVE-2024-43426, a flaw was discovered in pdfTeX, a widely used tool for processing TeX files. The issue stemmed from insufficient sanitization in the TeX notation filter, which led to a vulnerability allowing arbitrary file reads on systems where pdfTeX is available.
The vulnerability seems to be using the \pdffiledump
primitive, a command in pdfTeX that provides direct access to the contents of local files, as demonstrated in the Git commit shown below.
// the patch/commit tries to block \pdffiledump
@@ -84,7 +84,7 @@ function filter_tex_sanitize_formula(string $texexp): string {
'\afterassignment', '\expandafter', '\noexpand', '\special',
'\let', '\futurelet', '\else', '\fi', '\chardef', '\makeatletter', '\afterground',
'\noexpand', '\line', '\mathcode', '\item', '\section', '\mbox', '\declarerobustcommand',
- '\ExplSyntaxOn',
+ '\ExplSyntaxOn', '\pdffiledump',
];
Since pdfTeX is included in the default distribution of TeX Live, this vulnerability is a significant risk for systems using TeX Live with applications like Moodle. Without proper input sanitization, \pdffiledump
can be exploited to access local files, exposing sensitive data.
How is the primitives really being blocked?
As we can see in the patch above, it seems to be adding \pdffiledump
to a list of deny rules.
Here's a step-by-step breakdown of how the sanitization is implemented in the filter_tex_sanitize_formula()
function in /filter/tex/lib.php
.
The denylist is defined as an array of unsafe commands.
The patch includes \pdffiledump
in this denylist:
$denylist = [
'include', 'command', 'loop', 'repeat', 'open', 'toks', 'output',
'input', 'catcode', 'name', '^^',
'\def', '\edef', '\gdef', '\xdef',
'\every', '\errhelp', '\errorstopmode', '\scrollmode', '\nonstopmode',
'\batchmode', '\read', '\write', 'csname', '\newhelp', '\uppercase',
'\lowercase', '\relax', '\aftergroup',
'\afterassignment', '\expandafter', '\noexpand', '\special',
'\let', '\futurelet', '\else', '\fi', '\chardef', '\makeatletter', '\afterground',
'\noexpand', '\line', '\mathcode', '\item', '\section', '\mbox', '\declarerobustcommand',
'\ExplSyntaxOn', '\pdffiledump',
];
The denylist is then converted into regex patterns to match occurrences of unsafe commands in the input:
$denylist = array_map(function($value) {
return '/' . preg_quote($value, '/') . '/i';
}, $denylist);
For example, the entry \pdffiledump
becomes a case-insensitive regex pattern like /\\pdffiledump/i
.
Next, the function uses preg_replace_callback
to replace any denylisted keywords in the input TeX formula with the prefix forbiddenkeyword_
:
$texexp = preg_replace_callback($denylist, function($matches) {
error_log("FORBIDDEN: " . $matches[0]);
return 'forbiddenkeyword_' . $matches[0];
}, $texexp);
If the input formula contains \pdffiledump
, it will be replaced with forbiddenkeyword_\pdffiledump
, effectively preventing its execution (or is it?)
Finally, an allowlist is defined to prevent unintended blocking of safe commands. If a denylisted command was incorrectly mangled and is on the allowlist, it is restored:
$allowlist = ['inputenc'];
$allowlist = array_map(function($value) {
return '/\bforbiddenkeyword_(' . preg_quote($value, '/') . ')\b/i';
}, $allowlist);
$texexp = preg_replace_callback($allowlist, function($matches) {
return $matches[1];
}, $texexp);
Where does it fall short?
As we can see, if we enter the input \pdffiledump
, it will be converted to forbiddenkeyword_\pdffiledump
due to the sanitization process.
However, in TeX, the _
character is treated as a subscript operator. For example, in TeX, if you write text_32
, it will render as text
with 32
as a subscript (i.e., text₃₂
).
Similarly, when the sanitized string forbiddenkeyword_\pdffiledump
is processed by TeX, the _
causes it to treat forbiddenkeyword
as the main text and \pdffiledump
as the subscript.
Thus, this demonstrates that the patch is insufficient, as we are still able to read files via this primitive despite the intended sanitization.
Patched in Moodle ver. 4.5.2, 4.4.6, 4.3.10 and 4.1.16
The latest patch implements removal of backslashes to make commands impotent as shown here.
$texexp = preg_replace_callback($denylist,
function($matches) {
// Remove backslashes to make commands impotent.
$noslashes = str_replace('\\', '', $matches[0]);
return 'forbiddenkeyword_' . $noslashes;
},
$texexp
);
Thus, using \pdffiledump
returns forbiddenkeyword_pdffiledump
instead of forbiddenkeyword_\pdffiledump
. Safe to say, it’s fixed for now?
Timeline
- 16 December 2024 - Reported to Moodle via Bugcrowd.
- 9 Jan 2025 - Acknowledged by the developers.
- 18 Feb 2025 - Devs pushed a patch and the vulnerability was assigned CVE-2025-26525.