CVE-2026-69089
Grav CMS before 2.0.11 Path Traversal via watermark
Grav CMS 2.0.10 contains a path traversal vulnerability in ImageMedium::watermark(), which passes its unsanitized $image argument to RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator::findResource(). Because the file:// scheme branch only lexically collapses '..' segments without a realpath/containment check, an editor authoring Markdown image syntax with traversal sequences can cause arbitrary image files outside Grav's media sandbox to be composited into a carrier image, which is then cached and served from a public, unauthenticated URL — disclosing those files to anonymous visitors.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- Low
- Privileges needed
- None
- User interaction
- None
What an attacker can affect
- Confidentiality
- High
- Integrity
- None
- Availability
- None
Exploit likelihood
What each source reports
CVE List Score used
8.7
Grav CMS before 2.0.11 Path Traversal via watermark
Grav CMS 2.0.10 contains a path traversal vulnerability in ImageMedium::watermark(), which passes its unsanitized $image argument to RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator::findResource(). Because the file:// scheme branch only lexically collapses '..' segments without a realpath/containment check, an editor authoring Markdown image syntax with traversal sequences can cause arbitrary image files outside Grav's media sandbox to be composited into a carrier image, which is then cached and served from a public, unauthenticated URL — disclosing those files to anonymous visitors.
- Record
CVE-2026-69089- CVSS 4.0
- 8.7 High
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N - CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N - Weakness
- CWE-22
- Updated
- 2026-11-02
EUVDEUVD-2026-52282
8.7
Grav CMS 2.0.10 contains a path traversal vulnerability in ImageMedium::watermark(), which passes its unsanitized $image argument to RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator::findResource(). Because the file:// scheme branch only lexically collapses '..' segments without a realpath/containment check, an editor authoring Markdown image syntax with traversal sequences can cause arbitrary image files outside Grav's media sandbox to be composited into a carrier image, which is then cached and served from a public, unauthenticated URL — disclosing those files to anonymous visitors.
- Record
EUVD-2026-52282- CVSS 4.0
- 8.7 High
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-mmwh-j75q-gxp8
8.7
Duplicate Advisory: Grav: Path Traversal in ImageMedium::watermark() — arbitrary file disclosure via publicly-cached images
Duplicate Advisory
This advisory has been withdrawn because it is a duplicate of GHSA-w3f4-8pj2-599w. This link is maintained to preserve external references.
Original Description
Grav CMS 2.0.10 contains a path traversal vulnerability in ImageMedium::watermark(), which passes its unsanitized $image argument to RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator::findResource(). Because the file:// scheme branch only lexically collapses '..' segments without a realpath/containment check, an editor authoring Markdown image syntax with traversal sequences can cause arbitrary image files outside Grav's media sandbox to be composited into a carrier image, which is then cached and served from a public, unauthenticated URL — disclosing those files to anonymous visitors.
- Record
GHSA-mmwh-j75q-gxp8- CVSS 4.0
- 8.7 High
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X - CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N - Weakness
- CWE-22
- Updated
- 2026-11-02
GitHub AdvisoriesGHSA-w3f4-8pj2-599w
8.7
Grav: Path Traversal in ImageMedium::watermark() — arbitrary file disclosure via publicly-cached images
Reported by: Nihad Huseynli (@nihaddhuseynli (https://github.com/nihaddhuseynli)) — [email protected]
▎ Note: I attempted to report this via [email protected] first, per SECURITY.md, but the email bounced with 550 5.1.1 Address does not exist. Filing directly here instead.
Path Traversal in ImageMedium::watermark() leading to arbitrary file disclosure via publicly-served images
Summary
The watermark media action, documented and allow-listed for use in editor-authored Markdown image syntax, passes its $image argument unsanitized into UniformResourceLocator::findResource(). That resolver only lexically collapses .. segments (no realpath()/containment check) and, for the default file:// scheme, resolves straight to file_exists() with no re-validation against the registered stream root. A relative-path traversal string therefore resolves to an arbitrary absolute path on disk. If that path is a valid image, its pixel content is composited into the carrier image and the result is cached and served from a public, unauthenticated URL — i.e. any file outside Grav's media sandbox that happens to be a decodable image becomes visible to anonymous visitors, not just to the attacker.
Affected version
- Grav CMS, develop/2.0 line, commit db8c1fcd63aaaf6d6b244bc6b4cfa5f7b96bbc7f (tip of 2.0.11 post-release).
- Root cause lives in the pinned dependency rockettheme/toolbox v2.x-dev @ c569a53304cd7d95ff21bffa6fc590adcf0be83d (per composer.lock), specifically RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator.
- Not yet fixed as of this commit; unrelated to the four GHSA-* advisories already patched in 2.0.7–2.0.11 (which addressed arbitrary method-name dispatch, not this parameter-content issue).
Root cause
UniformResourceLocator::normalize() (ResourceLocator/src/UniformResourceLocator.php:261) cleans ../. segments purely as string manipulation against $this->base:
foreach ($parts as $i => $part) { if ($part === '..') { $part = array_pop($list); if ($part === null || $part === '' || (!$list && strpos($part, ':'))) { return false; // only refuses once popped past the leading sentinel } } ... }
Given enough ../ segments to match the depth of $this->base, this legitimately resolves to any absolute path on the filesystem, as string math. The file://-scheme branch of findCached() (UniformResourceLocator.php:476-493) then trusts that normalized path directly:
if ($scheme === 'file') { if (!$all && !file_exists($file)) { $this->cache[$key] = $array ? [] : false; } else { $this->cache[$key] = $array ? [$file] : $file; // <-- returned as-is } }
Unlike the else branch (find()), which re-glues resolved filenames onto a registered scheme root, the file:// branch performs no containment check.
ImageMedium::watermark() (system/src/Grav/Common/Page/Medium/ImageMedium.php:367) feeds attacker-influenced input straight into this resolver:
public function watermark($image = null, $position = null, $scale = null) { ... $args = func_get_args(); $file = $args[0] ?? '1'; $file = $file === '1' ? $config->get('system.images.watermark.image') : $args[0];
$watermark = $locator->findResource($file); // no path validation
$watermark = ImageFile::open($watermark); // decoded & composited
...
}
watermark is on Grav's own documented allow-list of Markdown image actions (Medium::ALLOWED_ACTIONS), so it is directly reachable through Excerpts::processMediaActions() (system/src/Grav/Common/Page/Markdown/Excerpts.php:262), which parses the querystring of any Markdown image reference and dispatches call_user_func_array([$medium, $action['method']], $args) for allow-listed methods — watermark's own parameter is never checked for path-safety anywhere in that chain.
Threat model
Per Grav's own SECURITY.md trust-boundary rubric: a publisher/editor (page-edit rights, no admin panel super-user access required) authors ordinary page…
- Record
GHSA-w3f4-8pj2-599w- CVSS 4.0
- 8.7 High
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N - Weakness
- CWE-22
- Updated
- 2026-11-02
NVD
8.7
Grav CMS 2.0.10 contains a path traversal vulnerability in ImageMedium::watermark(), which passes its unsanitized $image argument to RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator::findResource(). Because the file:// scheme branch only lexically collapses '..' segments without a realpath/containment check, an editor authoring Markdown image syntax with traversal sequences can cause arbitrary image files outside Grav's media sandbox to be composited into a carrier image, which is then cached and served from a public, unauthenticated URL — disclosing those files to anonymous visitors.
- Record
CVE-2026-69089- CVSS 4.0
- 8.7 High
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X - CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N - Weakness
- CWE-22
- Updated
- 2026-11-02
OSV
8.7
Grav CMS before 2.0.11 Path Traversal via watermark
Grav CMS 2.0.10 contains a path traversal vulnerability in ImageMedium::watermark(), which passes its unsanitized $image argument to RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator::findResource(). Because the file:// scheme branch only lexically collapses '..' segments without a realpath/containment check, an editor authoring Markdown image syntax with traversal sequences can cause arbitrary image files outside Grav's media sandbox to be composited into a carrier image, which is then cached and served from a public, unauthenticated URL — disclosing those files to anonymous visitors.
- Record
CVE-2026-69089- CVSS 4.0
- 8.7 High
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N - Weakness
- CWE-22
- Updated
- 2026-11-02
OSVGHSA-mmwh-j75q-gxp8
8.7
Duplicate Advisory: Grav: Path Traversal in ImageMedium::watermark() — arbitrary file disclosure via publicly-cached images
Duplicate Advisory
This advisory has been withdrawn because it is a duplicate of GHSA-w3f4-8pj2-599w. This link is maintained to preserve external references.
Original Description
Grav CMS 2.0.10 contains a path traversal vulnerability in ImageMedium::watermark(), which passes its unsanitized $image argument to RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator::findResource(). Because the file:// scheme branch only lexically collapses '..' segments without a realpath/containment check, an editor authoring Markdown image syntax with traversal sequences can cause arbitrary image files outside Grav's media sandbox to be composited into a carrier image, which is then cached and served from a public, unauthenticated URL — disclosing those files to anonymous visitors.
- Record
GHSA-mmwh-j75q-gxp8- CVSS 4.0
- 8.7 High
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X - CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N - Weakness
- CWE-22
- Updated
- 2026-11-02
OSVGHSA-w3f4-8pj2-599w
8.7
Grav: Path Traversal in ImageMedium::watermark() — arbitrary file disclosure via publicly-cached images
Reported by: Nihad Huseynli (@nihaddhuseynli (https://github.com/nihaddhuseynli)) — [email protected]
▎ Note: I attempted to report this via [email protected] first, per SECURITY.md, but the email bounced with 550 5.1.1 Address does not exist. Filing directly here instead.
Path Traversal in ImageMedium::watermark() leading to arbitrary file disclosure via publicly-served images
Summary
The watermark media action, documented and allow-listed for use in editor-authored Markdown image syntax, passes its $image argument unsanitized into UniformResourceLocator::findResource(). That resolver only lexically collapses .. segments (no realpath()/containment check) and, for the default file:// scheme, resolves straight to file_exists() with no re-validation against the registered stream root. A relative-path traversal string therefore resolves to an arbitrary absolute path on disk. If that path is a valid image, its pixel content is composited into the carrier image and the result is cached and served from a public, unauthenticated URL — i.e. any file outside Grav's media sandbox that happens to be a decodable image becomes visible to anonymous visitors, not just to the attacker.
Affected version
- Grav CMS, develop/2.0 line, commit db8c1fcd63aaaf6d6b244bc6b4cfa5f7b96bbc7f (tip of 2.0.11 post-release).
- Root cause lives in the pinned dependency rockettheme/toolbox v2.x-dev @ c569a53304cd7d95ff21bffa6fc590adcf0be83d (per composer.lock), specifically RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator.
- Not yet fixed as of this commit; unrelated to the four GHSA-* advisories already patched in 2.0.7–2.0.11 (which addressed arbitrary method-name dispatch, not this parameter-content issue).
Root cause
UniformResourceLocator::normalize() (ResourceLocator/src/UniformResourceLocator.php:261) cleans ../. segments purely as string manipulation against $this->base:
foreach ($parts as $i => $part) { if ($part === '..') { $part = array_pop($list); if ($part === null || $part === '' || (!$list && strpos($part, ':'))) { return false; // only refuses once popped past the leading sentinel } } ... }
Given enough ../ segments to match the depth of $this->base, this legitimately resolves to any absolute path on the filesystem, as string math. The file://-scheme branch of findCached() (UniformResourceLocator.php:476-493) then trusts that normalized path directly:
if ($scheme === 'file') { if (!$all && !file_exists($file)) { $this->cache[$key] = $array ? [] : false; } else { $this->cache[$key] = $array ? [$file] : $file; // <-- returned as-is } }
Unlike the else branch (find()), which re-glues resolved filenames onto a registered scheme root, the file:// branch performs no containment check.
ImageMedium::watermark() (system/src/Grav/Common/Page/Medium/ImageMedium.php:367) feeds attacker-influenced input straight into this resolver:
public function watermark($image = null, $position = null, $scale = null) { ... $args = func_get_args(); $file = $args[0] ?? '1'; $file = $file === '1' ? $config->get('system.images.watermark.image') : $args[0];
$watermark = $locator->findResource($file); // no path validation
$watermark = ImageFile::open($watermark); // decoded & composited
...
}
watermark is on Grav's own documented allow-list of Markdown image actions (Medium::ALLOWED_ACTIONS), so it is directly reachable through Excerpts::processMediaActions() (system/src/Grav/Common/Page/Markdown/Excerpts.php:262), which parses the querystring of any Markdown image reference and dispatches call_user_func_array([$medium, $action['method']], $args) for allow-listed methods — watermark's own parameter is never checked for path-safety anywhere in that chain.
Threat model
Per Grav's own SECURITY.md trust-boundary rubric: a publisher/editor (page-edit rights, no admin panel super-user access required) authors ordinary page…
- Record
GHSA-w3f4-8pj2-599w- CVSS 4.0
- 8.7 High
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N - Weakness
- CWE-22
- Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
getgrav/gravPackagist |
2.0.10 to 2.0.10 | 2.0.11 |
getgrav/gravPackagist |
2.0.10 up to 2.0.11 | 2.0.11 |
History
| Recorded | Changed | Sources |
|---|---|---|
| affected | ||
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-69089 is a high vulnerability (CVSS 8.7) in Getgrav Grav, published August 3, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.46%, the 40th percentile. Fixed in getgrav/grav 2.0.11.
References
github.com/getgrav/grav/security/advisories/GHSA-w3f4-8pj2-599w
github.com/getgrav/grav/commit/db8c1fcd63aaaf6d6b244bc6b4cfa5f7b96bbc7f
github.com/getgrav/grav/commit/c569a53304cd7d95ff21bffa6fc590adcf0be83d
github.com/getgrav/grav/commit/b282200a65ce979377963180629babd2335212ba
vulncheck.com/advisories/grav-cms-before-path-traversal-via-watermark
nvd.nist.gov/vuln/detail/CVE-2026-69089
Left out here: the rest of 2 source descriptions. The JSON record has them.