GHSA-c7r6-vx3h-w5g2: Path Traversal
Summary
Excel::store() resolved the destination path against the process working directory rather than the configured filesystem disk. When that path resolved to an existing file, the export was written straight to it with fopen(), bypassing the disk entirely. An application that passes a user-controlled value as the export path could therefore be made to overwrite an arbitrary existing file that the PHP process can write to, with content the user controls.
Details
Maatwebsite\Excel\Files\Disk::copy() contained two paths:
php if (realpath($destination)) { $tempStream = fopen($destination, 'rb+'); $success = streamcopytostream($readStream, $tempStream) !== false; } else { $success = $this->put($destination, $readStream); }
$destination is the $filePath argument given to Excel::store(), $export->store() or ->storeExcel(). realpath() resolves it against the current working directory — public/ for a typical web request — not against the disk root. On a hit, the write went directly to the filesystem and never reached Flysystem, which would otherwise have rejected ../ traversal and confined absolute paths to the disk root. The disk argument was effectively ignored for those paths, including for remote disks such as S3.
Two consequences follow:
the destination could be any existing file the PHP process can write, in or out of the disk root; the stream was opened 'rb+', which does not truncate, so a shorter export left trailing bytes of the previous file behind.
Because the file must already exist, the primitive is an overwrite rather than an arbitrary file creation. Overwriting a PHP file that is reachable by the web server (for example a front controller or a cached view) turns attacker-controlled row content into code execution, since CSV and HTML writers emit cell values verbatim. Passing an explicit writer type to store() bypasses the extension-based type detection that would otherwise reject a .php target.
Exploitation requires the application to pass an unsanitized, user-controlled value as the export path. Applications that pass a fixed or server-derived path are not affected.
Impact
Arbitrary overwrite of existing files writable by the PHP process, with partially attacker-controlled content, leading to remote code execution where the overwritten file is executed by the web server.
Patches
Fixed in 3.1.70. Disk::copy() now always writes through the configured filesystem disk, so Flysystem enforces the disk root for every export.
Note the behaviour change: passing an absolute path to store() previously wrote to that path once the file existed. Paths now always resolve relative to the disk root. Applications that relied on that should configure a disk rooted at the target location.
Workarounds
For anyone unable to upgrade, validate the path before passing it to store() — reject absolute paths and any .. segment, or derive the filename server-side and never build it from request input:
php $name = basename($request->input('filename')); // strips any directory part Excel::store($export, 'exports/' . $name, 'local');
Credit
Reported responsibly by @seck19 via the contact address in SECURITY.md.
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
composer/maatwebsite/excelto a version that resolves this vulnerability.Fixed in 3.1.70 - Upgrade
Upgrade
Maatwebsite Excelto a version that resolves this vulnerability.Fixed in 3.1.70 - Configuration
For any export call like `$export->store()` / `Excel::store(..., $diskOrPath)` / `->storeExcel()`, validate the destination path before passing it to `store()`: never pass absolute paths or paths containing `..`; build/derive the export filename server-side (do not use `request->input('filename')` or other user-controlled values directly).
PHP (Excel::store / writer invocation) Path validation for export destination = Reject absolute paths and any `..` segment; derive filename server-side (do not use request input) - Configuration
If you cannot upgrade to 3.1.70, avoid bypassing the filesystem disk: pass the export destination using the configured filesystem disk (and use an explicit writer type as described) so Flysystem enforces the disk root for every export and prevents escaping it.
Maatwebsite\Excel (store destination handling) Disk writer argument usage = Use explicit writer type / disk so Flysystem enforces the disk root for every export
Event History
Frequently Asked Questions
Which applications are exposed to this issue?
Applications are exposed if they pass a user-controlled value as the file path to Excel::store(), $export->store(), or storeExcel(). The issue can overwrite an existing file that the PHP process is permitted to write.
What conditions must an attacker satisfy to exploit it?
The attacker needs to control the export destination path and select a path that resolves to an existing file. The PHP process must have write permission to that target, and the attacker controls the export content written to it.
Does choosing a configured filesystem disk prevent the overwrite?
Not when the supplied destination resolves to an existing file. In that case, the code opens the resolved filesystem path directly with fopen() and bypasses Flysystem and the configured disk's path restrictions.
Where are affected paths resolved during a typical web request?
The destination is resolved relative to the PHP process current working directory rather than the configured disk root. For a typical web request, the working directory is described as public/.