Wysoka

Grav: Path Traversal in ImageMedium::watermark() — arbitrary file disclosure via publicly-cached images

  • Ujawniono: 17.09.2026
  • Aktualizacja: 11.10.2026 10:22

Reported by: Nihad Huseynli (@nihaddhuseynli (https://github.com/nihaddhuseynli)) — nihadd.huseynli@gmail.com

▎ Note: I attempted to report this via security@getgrav.org 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];

php
$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 content — the same trust tier already covered by the project's last four security advisories (GHSA-fj2p-qj2f-74v5, GHSA-c4wf-2xxc-68qm, GHSA-xwv3-2mv2-w33x, GHSA-ffmg-hfvg-jhg9). This is a new instance of that same "editor escapes their content sandbox" bug family, via an image-processing parameter rather than method-name dispatch.

Impact is not limited to the editor's own session: once the page is saved, any anonymous site visitor who requests the page causes the traversal to execute (if not already cached), and the resulting composited image is served from a public, unauthenticated cache URL.

Proof of Concept

Reproduced end-to-end against a clean local install of the affected commit (PHP 8.4.22, PHP built-in server, composer install --no-dev, bin/grav install).

  1. Outside the Grav webroot (one directory up), place a distinguishable "secret" image: a solid red 200x200 PNG, secret_outside_root.png.
  2. As an editor account (page-edit permission only, no admin.super), create a page with a solid blue 200x200 PNG carrier.png alongside it, and page content: carrier
  3. Any anonymous visitor requests the page: GET /poc. Grav renders an <img> tag pointing at a cached, public derivative URL, e.g. /images/b/2/8/2/2/b282200a65ce979377963180629babd2335212ba-carrier.png.
  4. Fetching that URL (again unauthenticated) and sampling pixels confirms the composited output contains the secret file's content: corner pixel (from carrier.png): RGB(0, 0, 255) — blue, expected center pixel (from secret_outside_root.png): RGB(255, 0, 0) — red, exfiltrated

(Test images and the exfiltrated output are attached separately — let me know if you need them regenerated.)

Suggested fix

  • In UniformResourceLocator::findCached()'s file:// branch, resolve the candidate path with realpath() and verify it remains inside $this->base before returning it — mirroring the containment that already exists implicitly in the non-file branch (find()).
  • Independently, in ImageMedium::watermark(), restrict $image to a filename (reject any value containing /, , or resolving outside user/pages/**/media and the configured watermark image root) before calling findResource().

Suggested severity

High — a lower-privilege actor's stored content results in exfiltration of data outside that actor's granted scope, and the exfiltrated data is exposed to anonymous third parties via a public cache URL, not just back to the attacker.

Czego dotyczy

Kliknij technologię, aby zobaczyć wszystkie dotyczące jej podatności.

PlatformaPaczkaPodatne wersjePoprawka w wersji
Paczka Composer getgrav/grav >= 2.0.10, < 2.0.11 2.0.11

Odnośniki

Ocena CVSS -
Wektor CVSS
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
Słabość (CWE)
CWE-22
Platforma
Composer
Paczka
getgrav/grav
Ujawniono
17.09.2026
Aktualizacja
11.10.2026 10:22

Źródła i licencje

OSV / GitHub Advisory Database - GHSA-w3f4-8pj2-599w - CC BY 4.0

Historia podatności: getgrav/grav

Wszystkie opisane podatności tej paczki (Paczka Composer), od najnowszej. Widać, czy to jednorazowa wpadka, czy paczka, której trzeba pilnować.

Wszystkie (99) w archiwum
Wszystkich wpisów
99
Pierwsza → ostatnia
11.2019 09.2026
Bez poprawki
6
Wykorzystywana w atakach
0

Rozkład istotności

  • Krytyczna 7
  • Wysoka 47
  • Średnia 41
  • Niska 4

Ujawnienia w latach

  1. 2019: 1
  2. 2020: 1
  3. 2021: 4
  4. 2022: 10
  5. 2023: 5
  6. 2024: 8
  7. 2025: 23
  8. 2026: 47
Istotność:
  1. Wysoka · 8.8 CVE-2026-72819 < 2.0.13 2.0.13
  2. Grav: Path Traversal in ImageMedium::watermark() — arbitrary file disclosure via publicly-cached images ten wpis
    Wysoka CVE-2026-69089 >= 2.0.10, < 2.0.11 2.0.11
  3. Wysoka CVE-2026-53653 >= 2.0.0-beta.1, < 2.0.0-rc.8, < 1.7.53 2.0.0-rc.8, 1.7.53
  4. Wysoka · 8.9 CVE-2026-42611 < 2.0.0-beta.2 2.0.0-beta.2
Pokaż więcej (20) Widać 20 z 54

Dowiedz się, które podatności dotyczą Twojej aplikacji

Po podłączeniu integracji DockRay i włączeniu raportowania oprogramowania DockRay porównuje zgłoszone wersje komponentów ze znanymi podatnościami i powiadamia o potwierdzonych dopasowaniach. Zakres zależy od obsługiwanej technologii i dostępnych biuletynów.

Podłącz swoją aplikację
Newsletter DockRay

Bezpieczeństwo aplikacji w Twojej skrzynce

Najważniejsze podatności, konkretne zalecenia i praktyczne wskazówki dla Twoich aplikacji. Wybierz technologie i śledź to, co ma znaczenie dla Twojego projektu.

  • Codzienne zestawienie nowych podatności, także krytycznych.
  • Powiadomienia o ważnych aktualizacjach.
  • Praktyczne porady o bezpieczeństwie i monitoringu aplikacji.

Pozostaw puste, aby otrzymywać informacje o wszystkich technologiach.

Zapis jest bezpłatny. Wypiszesz się jednym kliknięciem w wiadomości.