Описание
Grav: Unauthenticated Path Traversal via Missing Directory-Boundary Check in plugin-asset-map.php Static Asset Server (index.php)
Verified against: getgrav/grav devel branch, GRAV_VERSION = "2.0.15", file `index.php
Title
Unauthenticated Path Traversal via Missing Directory-Boundary Check in plugin-asset-map.php Static Asset Server (index.php)
Product / Affected Versions
- Product:
getgrav/grav - File:
index.php(top-level front controller, runs before Grav itself boots) - Confirmed present in: devel branch, 2.0.15
- Precondition: requires
user/config/plugin-asset-map.phpto exist and contain at least one route-prefix mapping ,this is an opt-in mechanism (per the code comment: "Fast static asset serving for plugins that bundle SPA apps"). No core mechanism generates this file automatically; it's created by a plugin that opts into this fast-path. Not reachable on a stock Grav install with no such plugin. Where reachable, it requires zero authentication.
CWE
CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') , specific mechanism: a path-prefix containment check performed with plain string comparison (str_starts_with) instead of a directory-boundary-aware comparison, allowing escape into any sibling path whose name happens to extend the base directory's name as a string.
Description
index.php implements a fast-path static file server that runs before Grav's own routing/security stack, gated on the presence of an asset-map file:
realpath() correctly resolves .. sequences, so a naive ../../etc/passwd-style traversal that leaves the filesystem entirely is blocked (it wouldn't share the $realBase string prefix). But the containment check itself, str_starts_with($realFile, $realBase), has no directory-boundary awareness it's a plain string-prefix test, not "is $realFile inside the $realBase directory." Any resolved path whose string representation merely begins with the same characters as $realBase passes, including sibling directories that extend the base directory's name (assets → assets-secret, assets.bak, assets_old, assets2, etc.) a very common real-world directory-naming pattern (backup dirs, versioned dirs, disabled/legacy dirs sitting alongside the active one).
Live Proof of Concept
Setup: the exact code block above, extracted verbatim from index.php, executed with PHP 8.3.6 against a realistic directory layout (a plugin's active assets/ dir sitting next to an unrelated assets-secret/ dir containing a fake secret):
Legitimate request (/myplugin-assets/app.js):
Traversal request (/myplugin-assets/../assets-secret/config.php):
str_starts_with('.../assets-secret/config.php', '.../assets') evaluates true because assets-secret literally begins with the characters assets there is no separator-boundary check (e.g. requiring $realBase . '/' as the actual prefix) to prevent this.
Trust-boundary framing (per Grav's own SECURITY.md)
This code path requires no Grav account , it runs before Grav even initializes, directly off the raw request path. Per Grav's own stated criteria: "An unauthenticated attacker can achieve RCE, exfiltrate site data, or gain admin-equivalent control. No Grav account required" → this matches the CRITICAL bar exactly, for any deployment where the plugin-asset-map.php mechanism is in active use.
Suggested Fix
Append a trailing directory separator before the prefix comparison, or use a proper containment check:
This is the standard fix for this exact bug class , ensuring the matched prefix ends exactly at a directory boundary, not partway through a longer sibling name.
Пакеты
getgrav/grav
<= 2.0.14
2.0.15
Связанные уязвимости
Grav before 2.0.15 contains a path traversal vulnerability in the static asset server within index.php that uses string prefix matching instead of directory-boundary validation. Unauthenticated attackers can access files in sibling directories by exploiting directory names that extend the base path string, such as requesting assets-secret when assets is the configured base.