Moodle: moodle: uncontrolled resource consumption in tex formula editor leading to denial of service
Description
A denial-of-service vulnerability was identified in Moodle’s TeX formula editor. When rendering TeX content using mimetex, insufficient execution time limits could allow specially crafted formulas to consume excessive server resources. An authenticated user could abuse this behavior to degrade performance or cause service interruption.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Moodle's TeX formula editor vulnerable to DoS via crafted formulas due to missing mimetex execution timeout.
Vulnerability
CVE-2026-26047 is a denial-of-service vulnerability in Moodle's TeX formula editor. When rendering TeX content using mimetex, there were insufficient execution time limits, allowing specially crafted formulas to consume excessive server resources. The root cause is the absence of a timeout for the mimetex command execution, which could be triggered by an authenticated user [1][2].
Exploitation
An authenticated user can submit a maliciously crafted TeX formula through the equation editor. Without a timeout, the mimetex process may run indefinitely or for an extended period, leading to CPU and memory exhaustion. The attack requires authentication but no special privileges, as any user with access to the editor can exploit it [2].
Impact
Successful exploitation degrades server performance and can cause a complete service interruption, affecting all users. The availability of the Moodle instance is compromised, potentially disrupting learning activities [1][2].
Mitigation
The vulnerability is patched in Moodle versions that include commit 8683b4a, which adds a configurable timeout (default 5 seconds) for mimetex execution. Administrators should update to the latest version or apply the patch [3][4]. No workarounds are documented; the fix is the recommended course of action.
AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
moodle/moodlePackagist | >= 5.1.0-beta, < 5.1.2 | 5.1.2 |
moodle/moodlePackagist | >= 5.0.0-beta, < 5.0.5 | 5.0.5 |
moodle/moodlePackagist | < 4.5.9 | 4.5.9 |
Affected products
1Patches
18683b4a04939MDL-86785 tiny_equation: Add timeout handling for mimetex execution
3 files changed · +187 −5
public/filter/tex/lib.php+181 −2 modified@@ -26,10 +26,22 @@ defined('MOODLE_INTERNAL') || die(); +// Default timeout in seconds for mimetex command execution. +defined('FILTER_TEX_MIMETEX_TIMEOUT') || define('FILTER_TEX_MIMETEX_TIMEOUT', 5); + +/** + * Check if the current operating system is Windows. + * + * @return bool True if running on Windows, false otherwise. + */ +function filter_tex_is_windows(): bool { + return (PHP_OS == "WINNT") || (PHP_OS == "WIN32") || (PHP_OS == "Windows"); +} + function filter_tex_get_executable($debug=false) { global $CFG; - if ((PHP_OS == "WINNT") || (PHP_OS == "WIN32") || (PHP_OS == "Windows")) { + if (filter_tex_is_windows()) { return "$CFG->dirroot/filter/tex/mimetex.exe"; } @@ -134,7 +146,7 @@ function filter_tex_get_cmd($pathname, $texexp) { $texexp = escapeshellarg($texexp); $executable = filter_tex_get_executable(false); - if ((PHP_OS == "WINNT") || (PHP_OS == "WIN32") || (PHP_OS == "Windows")) { + if (filter_tex_is_windows()) { $executable = str_replace(' ', '^ ', $executable); return "$executable ++ -e \"$pathname\" -- $texexp"; @@ -143,6 +155,173 @@ function filter_tex_get_cmd($pathname, $texexp) { } } +/** + * Run mimetex command with a timeout on Windows. + * + * @param string $cmd Command string to execute. + * @param int $timeoutmicros Timeout in microseconds. + * @return array Array with keys: code, timedout, status, errors. + */ +function filter_tex_exec_windows(string $cmd, int $timeoutmicros): array { + // Create temporary file for stderr. + $temperr = tempnam(sys_get_temp_dir(), 'err_'); + + $descriptors = [ + 0 => ['file', 'NUL', 'r'], // STDIN. + 1 => ['file', 'NUL', 'w'], // STDOUT. + 2 => ['file', $temperr, 'w'], // STDERR. + ]; + + $process = proc_open($cmd, $descriptors, $pipes); + if (!is_resource($process)) { + unlink($temperr); + return [ + 'code' => 127, // Command not found. + 'timedout' => false, + 'status' => [], + 'errors' => '', + ]; + } + + $timedout = false; + while ($timeoutmicros > 0) { + $start = microtime(true); + $status = proc_get_status($process); + + if (!$status['running']) { + break; + } + + $timeoutmicros -= (microtime(true) - $start) * 1000000; + if ($timeoutmicros <= 0) { + $timedout = true; + $pid = (int)($status['pid'] ?? 0); + exec('taskkill /F /T /PID ' . $pid . ' 2>NUL'); + break; + } + + usleep(50000); // Sleep for 50ms. + } + + $status = proc_get_status($process); + $code = proc_close($process); + + // Capture stderr from temp file. + $errors = file_get_contents($temperr); + unlink($temperr); + + return [ + 'code' => $code, + 'timedout' => $timedout, + 'status' => $status, + 'errors' => $errors, + ]; +} + +/** + * Run mimetex command with a timeout on Unix-like systems. + * + * @param string $cmd Command string to execute. + * @param int $timeoutmicros Timeout in microseconds. + * @return array Array with keys: code, timedout, status, errors. + */ +function filter_tex_exec_unix(string $cmd, int $timeoutmicros): array { + // File descriptors passed to the process. + $descriptors = [ + 0 => ['pipe', 'r'], // STDIN. + 1 => ['pipe', 'w'], // STDOUT. + 2 => ['pipe', 'w'], // STDERR. + ]; + + $process = proc_open('exec ' . $cmd, $descriptors, $pipes); + if (!is_resource($process)) { + return [ + 'code' => 127, // Command not found. + 'timedout' => false, + 'status' => [], + 'errors' => '', + ]; + } + + fclose($pipes[0]); + stream_set_blocking($pipes[1], false); + stream_set_blocking($pipes[2], false); + + $errors = ''; + $timedout = false; + while ($timeoutmicros > 0) { + $start = microtime(true); + + $read = [$pipes[1], $pipes[2]]; + $other = []; + stream_select($read, $other, $other, 0, (int)$timeoutmicros); + + $status = proc_get_status($process); + + stream_get_contents($pipes[1]); // Discard stdout to prevent pipe blocking. + $errors .= stream_get_contents($pipes[2]); + + if (!$status['running']) { + break; + } + + $timeoutmicros -= (microtime(true) - $start) * 1000000; + if ($timeoutmicros <= 0) { + $timedout = true; + proc_terminate($process); + break; + } + + usleep(50000); // Sleep for 50ms. + } + + // Read any remaining data from pipes. + stream_get_contents($pipes[1]); // Discard remaining stdout. + $errors .= stream_get_contents($pipes[2]); + + fclose($pipes[1]); + fclose($pipes[2]); + + $status = proc_get_status($process); + $code = proc_close($process); + + return [ + 'code' => $code, + 'timedout' => $timedout, + 'status' => $status, + 'errors' => $errors, + ]; +} + +/** + * Run mimetex command with a timeout. + * + * @param string $cmd Command string to execute. + * @param int|null &$code Exit code (passed by reference, set by function). + * @return void + */ +function filter_tex_exec(string $cmd, ?int &$code): void { + $timeoutmicros = FILTER_TEX_MIMETEX_TIMEOUT * 1000000; + + if (filter_tex_is_windows()) { + $result = filter_tex_exec_windows($cmd, $timeoutmicros); + } else { + $result = filter_tex_exec_unix($cmd, $timeoutmicros); + } + + if ($result['errors']) { + debugging('filter_tex_exec errors: ' . $result['errors'], DEBUG_DEVELOPER); + } + + if ($result['timedout']) { + $code = 124; + } else if ($result['code'] === -1 && isset($result['status']['exitcode']) && $result['status']['exitcode'] !== -1) { + $code = $result['status']['exitcode']; + } else { + $code = $result['code']; + } +} + /** * Purge all caches when settings changed. */
public/filter/tex/pix.php+1 −2 modified@@ -18,7 +18,6 @@ require_once($CFG->dirroot.'/filter/tex/latex.php'); $cmd = ''; // Initialise these variables - $status = ''; $relativepath = get_file_argument(); @@ -58,7 +57,7 @@ $texexp = preg_replace('!\r\n?!', ' ', $texexp); $texexp = '\Large '.$texexp; $cmd = filter_tex_get_cmd($pathname, $texexp); - system($cmd, $status); + filter_tex_exec($cmd, $status); } } }
public/filter/tex/texdebug.php+5 −1 modified@@ -153,7 +153,7 @@ function tex2image($texexp, $return=false) { $texexp = '\Large '.$texexp; $commandpath = filter_tex_get_executable(true); $cmd = filter_tex_get_cmd($pathname, $texexp); - system($cmd, $status); + filter_tex_exec($cmd, $status); if ($return) { return $image; @@ -172,6 +172,10 @@ function tex2image($texexp, $return=false) { echo "Status corresponds to bus error<br />\n"; } else if ($status == 22) { echo "Status corresponds to abnormal termination<br />\n"; + } else if ($status == 124) { + echo "Status corresponds to timeout<br />\n"; + } else if ($status == 127) { + echo "Status corresponds to command not found<br />\n"; } if (file_exists($commandpath)) { echo "File size of mimetex executable $commandpath is " . filesize($commandpath) . "<br />";
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-cg8j-5cr2-568qghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-26047ghsaADVISORY
- access.redhat.com/security/cve/CVE-2026-26047ghsavdb-entryx_refsource_REDHATWEB
- bugzilla.redhat.com/show_bug.cgighsaissue-trackingx_refsource_REDHATWEB
- github.com/moodle/moodle/commit/8683b4a04939332e353cad1be51222930dc40b2cghsaWEB
- moodle.org/mod/forum/discuss.phpghsaWEB
News mentions
0No linked articles in our index yet.