
Loading

Loading
We use strictly necessary cookies to run this site, and analytics cookies to understand how it's used. See our Privacy Policy for details.
A GitHub-reviewed security advisory, not necessarily under active exploitation the way our CISA KEV advisories are, but a real, disclosed vulnerability in an open-source dependency.
Affected packages
Vulnerable: <= 2.0.15
Patched in 2.0.16
Details
A path traversal vulnerability in MediaUploadTrait::deleteFile() allows an authenticated user with media management permissions to delete arbitrary files on the server. The method validates only the basename portion of the filename using Utils::checkFilename(), while the directory path (which may contain ../ sequences) is preserved and passed unvalidated to unlink(). This enables directory escape from the intended media storage path.
High (8.1) - CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H
CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
In system/src/Grav/Common/Media/Traits/MediaUploadTrait.php, the deleteFile() method (lines 332-365) performs filename validation only on the basename, not the full path:
public function deleteFile(string $filename, ?array $settings = null): void
{
$settings = $this->getUploadSettings($settings);
$filesystem = Filesystem::getInstance(false);
// Line 339-340: Only the BASENAME is validated
$basename = $filesystem->basename($filename); // e.g. "evil.jpg" from "../../evil.jpg"
if (!Utils::checkFilename($basename)) { // passes - no traversal in basename
throw new RuntimeException(/* ... */);
}
$path = $settings['destination'] ?? $this->getPath();
// ...
// Line 353: Full pathname (with traversal) is preserved
$pathname = $filesystem->pathname($filename); // "../../"
// Line 356-357: Traversal path reconstructed
[$base, $ext,,] = $this->getFileParts($basename);
$name = "{$pathname}{$base}.{$ext}"; // "../../evil.jpg"
// Line 360: Passed to doRemove()
$this->doRemove($name, $path);
}
doRemove() (line 521-582) then calls:
// Line 538
unlink("{$folder}/{$filename}");
// e.g. unlink("/var/www/grav/user/pages/mypage/../../config/system.yaml")
Utils::checkFilename() (lines 1022-1044) properly checks for /, \, and .., but it is applied to $filesystem->basename($filename) (the last path component only), so traversal sequences in the directory portion are never validated.
The vulnerability is reachable through the Flex media handling pipeline:
FlexMediaTrait::setUpdatedMedia() (line 386) iterates form flash data where $filename is the array key - user-controlled$file is null, line 396), NO upload validation is performed (the checkUploadedFile() call at line 401 only executes when $file is truthy)$this->_uploads at line 414saveUpdatedMedia() (line 499) calls $media->deleteFile($filename, $settings) with the unsanitized filenameThe same pattern exists in renameFile() (lines 374-405) which has even weaker validation - it performs NO checkFilename() call at all. While renameFile() currently has no callers in the core codebase, it is part of the public MediaUploadInterface and should be fixed as defense-in-depth.
Environment: Grav CMS 2.0.16 with admin plugin
The attack requires an authenticated admin user with page/media editing permissions (not super-admin).
echo "DELETE_ME" > /var/www/grav/user/data/target.txt
POST /admin/pages/mypage/task:save
Content-Type: multipart/form-data
# The form flash data includes a media deletion entry with key:
# "../../data/target.txt" -> null (deletion marker)
When saveUpdatedMedia() processes the deletion queue:
$filename = ../../data/target.txtdeleteFile("../../data/target.txt") is called$basename = target.txt (passes checkFilename())$pathname = ../../data/$name = ../../data/target.txtdoRemove() calls unlink("/var/www/grav/user/pages/mypage/../../data/target.txt")unlink("/var/www/grav/user/data/target.txt")The file is deleted outside the intended media directory.
An authenticated user with media management permissions can:
user/config/system.yaml, user/config/security.yaml)Apply Utils::checkFilename() to the full $filename parameter before decomposing it, or reject any filename containing directory separators or .. sequences:
public function deleteFile(string $filename, ?array $settings = null): void
{
$settings = $this->getUploadSettings($settings);
$filesystem = Filesystem::getInstance(false);
// Validate the FULL filename, not just the basename
if (!Utils::checkFilename($filename)) {
throw new RuntimeException(/* ... */);
}
// ... rest unchanged
}
The same fix should be applied to renameFile() for both $from and $to parameters.
system/src/Grav/Common/Media/Traits/MediaUploadTrait.php lines 332-365, 521-582system/src/Grav/Framework/Flex/Traits/FlexMediaTrait.php lines 386-414, 490-499system/src/Grav/Common/Media/Traits/MediaUploadTrait.php lines 374-405 (renameFile)This vulnerability was discovered using AI-assisted security research tools.
EPSS, exploit probability
Low0.63%
estimated chance of real-world exploitation in the next 30 days, higher than 50.2% of every CVE FIRST.org scores
Refreshed 11/2/2026, via FIRST.org's EPSS model, not CVSS, this measures likelihood of exploitation, not how severe it would be.
Talk to us about this
Find out whether this dependency (or others like it) is actually reachable in your codebase.
Get help patching, or building dependency-scanning into your pipeline going forward.
A secure code review or supply chain review finds this before an advisory does.