Where
AND
-Infinity
0
Severity
4.9
Path Traversal
AV:N/AC:L/PR:H/UI:N/S:C/C:L/I:N/A:N

Summary

Users with the role System-Admin (ROLESYSTEADMIN) and the permission uploadinvoicetemplate can upload PDF invoice templates, which can call pdfContext.setOption('associatedfiles', ...) inside the sandboxed Twig render.

This is forwarded to mPDF's SetAssociatedFiles(), whose writer calls filegetcontents($entry['path']) during PDF output and embeds the bytes as a FlateDecode stream in the PDF. Any file readable by the PHP worker is returned to the attacker inside the rendered invoice.

Root cause

1. src/Twig/SecurityPolicy/StrictPolicy.php:123-128 explicitly whitelists PdfContext::setOption(): php if ($obj instanceof PdfContext) { if ($lcm !== 'setoption') { throw ...; } return; }

2. src/Pdf/MPdfConverter.php keeps associatedfiles in the pass-through allowlist: php $allowed = ['mode','format','defaultfontsize','defaultfont', ... , 'associatedfiles','additionalxmprdf']; and then forwards it to mPDF: php if (arraykeyexists('associatedfiles', $options) && isarray($options['associatedfiles'])) { $associatedFiles = $options['associatedfiles']; unset($options['associatedfiles']); } ... $mpdf->SetAssociatedFiles($associatedFiles);

3. mPDF 8.3.1 MetadataWriter::writeAssociatedFiles() calls filegetcontents, which respects PHP stream wrappers: php if (isset($file['path'])) { $fileContent = @filegetcontents($file['path']); } ... $filestream = gzcompress($fileContent); $this->writer->write('<</Type /EmbeddedFile');

The sandbox and the option allowlist were both written defensively (short whitelists, not blacklists), but neither side considered that associatedfiles is a PDF/A file-embedding feature whose path key is a sink.

Fix

The implemented fix has two aspects:

1. The PdfContext now works with a strict allow-list, that excludes associatedfiles 2. The MPdfConverter now removes any path from the $associatedFiles array, which can still be used by plugins: php if (\count($associatedFiles) > 0) { // remove "path" so mPDF will not use filegetcontents() on local files // callers must pre-read and pass the bytes via "content" $associatedFiles = arraymap(static function ($entry): array { if (!\isarray($entry)) { return []; }

if (\arraykeyexists('path', $entry)) { unset($entry['path']); }

return $entry; }, $associatedFiles); $mpdf->SetAssociatedFiles($associatedFiles); }

1 / 2
Source: GitHub
First published (updated )

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203