CVE-2026-69089: Grav CMS Path Traversal via ImageMedium::watermark()
A Grav CMS editor can read image files anywhere on the server by pointing the watermark action at a relative path like ../secret.png, and the composited result ends up cached at a public URL any…
The problem
Grav 2.0.10 lets editors use the watermark query parameter in Markdown image syntax to composite a second image onto a carrier. The $image argument is passed directly to UniformResourceLocator::findResource() with no path validation.
The file:// scheme branch of that resolver collapses ../ segments with pure string math against $this->base, then calls file_exists() on the result and returns it as-is. There is no realpath() call and no check that the resolved path stays inside the media sandbox.
Any image-decodable file the web-server process can read becomes reachable, and the composited output is saved to a public, unauthenticated cache URL, exposing the content to anonymous visitors, not just the attacker.
Proof of concept
A working proof-of-concept for CVE-2026-69089 in getgrav/grav, with the exact payload below.
watermark is on Grav's own ALLOWED_ACTIONS list, so Excerpts::processMediaActions() dispatches it via call_user_func_array() without any parameter inspection. Inside watermark(), $args[0] feeds straight into $locator->findResource(), which in the file:// branch performs only lexical ../ collapsing (UniformResourceLocator::normalize()) and no containment check before returning the path.
The number of ../ segments needed equals the depth of the page's media directory below the webroot, typically two or three. Once the traversal resolves to a valid image, ImageFile::open() decodes and composites it, and the result is written to the public /images/ cache.
The patch (commit b282200) adds a containment guard in watermark() so editor-supplied paths are restricted to the site's own media tree; operator-configured watermarks and stream URIs are unaffected. Root cause is CWE-22.
The fix
Upgrade to Grav CMS 2.0.11. The fix (commits b282200, c569a53, db8c1fc) adds a path-containment check in ImageMedium::watermark() so editor-supplied watermark paths are confined to the site's media directories. As a temporary workaround, restrict all accounts to read-only page access and remove page-edit permissions until the upgrade is applied.
Reported by Nihad Huseynli (@nihaddhuseynli).
Related research
- high · 8.8CVE-2026-65608CVE-2026-65608: Grav FlexDirectory Unsafe Reflection RCE
- high · 7.4CVE-2026-62669CVE-2026-62669: Grav CMS 2FA Bypass via Unauthenticated Secret Rotation
- highCVE-2026-64850CVE-2026-64850: Grav CMS Remote Code Execution via Unrestricted Callable in Blueprint::dynamicData()
- highCVE-2026-62673CVE-2026-62673: Grav .htaccess File Extension Filter Case-Sensitivity Bypass