# Grav: media_directory() Twig function allows filesystem path traversal and file content disclosure from sandboxed page content

Grav's sandboxed media_directory() Twig function skips containment checks for plain filesystem paths, letting page authors enumerate and read arbitrary web-server-readable files via the allow-listed Medium.filepath accessor.

- Payout: Research
- Severity: high
- Program: Grav CMS
- Found by: alham-rizvi
- Disclosed: 2026-09-17
- CVE: CVE-2026-72697
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-47ch-6w46-6xm7
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-47ch-6w46-6xm7
- Tags: area:web domain:web impact:data-leak kind:research platform:ghsa sev:high tech:php technique:incomplete-fix technique:source-review vuln:info-disclosure vuln:path-traversal vuln:sandbox-escape

**Target:** Twig sandbox function media_directory() / Grav\Common\Page\Media
**Root cause:** mediaDirFunc() only resolves/validates input when UniformResourceLocator::isStream() returns true; plain absolute or relative filesystem paths fall through unchecked into new Media($path). Compounding this, the sandbox's Medium allow-list includes filepath despite a comment stating it should stay blocked, exposing the absolute path of each discovered file.
**Steps:**
1. Confirm affected version <= 2.0.15 (fixed 2.0.16).
2. Author page content processed as Twig (modular page, or frontmatter `process.twig: true`).
3. Call `{{ media_directory('/etc').files }}` (or any web-server-readable directory) — isStream() is false so the raw path reaches `new Media($path)`.
4. Media::init() iterates the directory with FilesystemIterator and keeps files whose extension matches `media.types` (default includes txt, json, xml, pdf, doc, docx).
5. Read each item's `.filepath` (allow-listed for Medium) to get the absolute path, then read its bytes.
**How found:** Code review of the sandbox allow-list: noticed mediaDirFunc() gates only on isStream(), so non-stream paths bypass validation, and spotted the security.yaml comment declaring filepath 'dangerous surface... stays blocked' contradicted by filepath being listed as an allowed Medium method on the next line. Verified with a harness using the real pinned Grav/toolbox/Pimple/psr-container commits.
**Exploitation:** A low-privileged page author supplies a plain filesystem path to media_directory(); the sandbox permits the function and the Medium.filepath accessor, so the template enumerates matching files and resolves their absolute paths, then reads contents. Bounded only by web-server OS permissions and the media extension filter.
**Escalation:** On shared hosting the same primitive reaches other tenants' files; within one install it escapes the user:///theme:// scope the sandbox intends to enforce.
**Insight:** When a sandbox allow-list has a comment enumerating 'dangerous' methods, diff that comment against the actual methods list — a contradiction there is a direct capability leak. Also check whether path-validation helpers gate on a stream/URL predicate, since plain filesystem paths then skip the check entirely.

