{"id":"CVE-2026-69089","aliases":["EUVD-2026-52282","GHSA-mmwh-j75q-gxp8","GHSA-w3f4-8pj2-599w"],"status":"published","title":"Grav CMS before 2.0.11 Path Traversal via watermark","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.","severity":{"version":"4.0","score":8.7,"vector":"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","level":"high","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-10","score":0.00459,"percentile":0.40088,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-22"],"published":"2026-08-03T13:20:43Z","modified":"2026-10-28T02:31:04Z","fixes":[{"ecosystem":"Packagist","package":"getgrav/grav","fixed_in":["2.0.11"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"Packagist","package":"getgrav/grav","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"2.0.10"},{"last_affected":"2.0.10"},{"fixed":"2.0.11"}]}],"versions":["2.0.10"]},{"source":"osv","ecosystem":"Packagist","package":"getgrav/grav","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"2.0.10"},{"fixed":"2.0.11"}]}],"versions":["2.0.10"]}],"products":[{"vendor":"getgrav","product":"grav"}],"references":[{"url":"https://github.com/getgrav/grav/security/advisories/GHSA-w3f4-8pj2-599w","tags":["vendor-advisory","exploit","advisory","web"]},{"url":"https://github.com/getgrav/grav/commit/db8c1fcd63aaaf6d6b244bc6b4cfa5f7b96bbc7f","tags":["patch","fix","web"]},{"url":"https://github.com/getgrav/grav/commit/c569a53304cd7d95ff21bffa6fc590adcf0be83d","tags":["patch","fix","web"]},{"url":"https://github.com/getgrav/grav/commit/b282200a65ce979377963180629babd2335212ba","tags":["patch","fix","web"]},{"url":"https://www.vulncheck.com/advisories/grav-cms-before-path-traversal-via-watermark","tags":["third-party-advisory","advisory","web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-69089","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-mmwh-j75q-gxp8"},{"url":"https://github.com/advisories/GHSA-w3f4-8pj2-599w"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/69xxx/CVE-2026-69089.json","tags":["advisory"]},{"url":"https://github.com/getgrav/grav","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-69089","title":"Grav CMS before 2.0.11 Path Traversal via watermark","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.","status":"published","published":"2026-08-03T13:20:43.593Z","modified":"2026-10-08T15:24:19.204Z","cvss":[{"version":"4.0","score":8.7,"vector":"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","level":"high","assessor":"cna"},{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N","level":"high","assessor":"cna"}],"cwes":["CWE-22"],"updated_at":"2026-11-02T13:50:27.582139Z"}],"euvd":[{"id":"EUVD-2026-52282","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.","status":"published","published":"2026-08-03T13:20:43Z","modified":"2026-10-08T15:24:19Z","cvss":[{"version":"4.0","score":8.7,"vector":"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","level":"high"}],"updated_at":"2026-11-02T17:27:31.218078Z"}],"ghsa":[{"id":"GHSA-mmwh-j75q-gxp8","title":"Duplicate Advisory: Grav: Path Traversal in ImageMedium::watermark() — arbitrary file disclosure via publicly-cached images","description":"### Duplicate Advisory\nThis advisory has been withdrawn because it is a duplicate of GHSA-w3f4-8pj2-599w. This link is maintained to preserve external references.\n\n### Original Description\nGrav 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.","status":"withdrawn","published":"2026-08-03T15:32:48Z","modified":"2026-09-17T17:16:44Z","cvss":[{"version":"4.0","score":8.7,"vector":"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","level":"high"},{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N","level":"high"}],"cwes":["CWE-22"],"updated_at":"2026-11-02T14:41:18.357514Z"},{"id":"GHSA-w3f4-8pj2-599w","title":"Grav: Path Traversal in ImageMedium::watermark() — arbitrary file disclosure via publicly-cached images","description":"Reported by: Nihad Huseynli (@nihaddhuseynli (https://github.com/nihaddhuseynli)) — nihadd.huseynli@gmail.com\n\n▎ 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.\n\nPath Traversal in ImageMedium::watermark() leading to arbitrary file disclosure via publicly-served images\n\nSummary\n\nThe 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.\n\nAffected version\n\n- Grav CMS, develop/2.0 line, commit db8c1fcd63aaaf6d6b244bc6b4cfa5f7b96bbc7f (tip of 2.0.11 post-release).\n- Root cause lives in the pinned dependency rockettheme/toolbox v2.x-dev @ c569a53304cd7d95ff21bffa6fc590adcf0be83d (per composer.lock), specifically RocketTheme\\Toolbox\\ResourceLocator\\UniformResourceLocator.\n- 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).\n\nRoot cause\n\nUniformResourceLocator::normalize() (ResourceLocator/src/UniformResourceLocator.php:261) cleans ../. segments purely as string manipulation against $this-\u003ebase:\n\nforeach ($parts as $i =\u003e $part) {\n    if ($part === '..') {\n        $part = array_pop($list);\n        if ($part === null || $part === '' || (!$list \u0026\u0026 strpos($part, ':'))) {\n            return false; // only refuses once popped past the leading sentinel\n        }\n    } ...\n}\n\nGiven enough ../ segments to match the depth of $this-\u003ebase, 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:\n\nif ($scheme === 'file') {\n    if (!$all \u0026\u0026 !file_exists($file)) {\n        $this-\u003ecache[$key] = $array ? [] : false;\n    } else {\n        $this-\u003ecache[$key] = $array ? [$file] : $file;   // \u003c-- returned as-is\n    }\n}\n\nUnlike the else branch (find()), which re-glues resolved filenames onto a registered scheme root, the file:// branch performs no containment check.\n\nImageMedium::watermark() (system/src/Grav/Common/Page/Medium/ImageMedium.php:367) feeds attacker-influenced input straight into this resolver:\n\npublic function watermark($image = null, $position = null, $scale = null)\n{\n    ...\n    $args = func_get_args();\n    $file = $args[0] ?? '1';\n    $file = $file === '1' ? $config-\u003eget('system.images.watermark.image') : $args[0];\n\n    $watermark = $locator-\u003efindResource($file);   // no path validation\n    $watermark = ImageFile::open($watermark);      // decoded \u0026 composited\n    ...\n}\n\nwatermark 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.\n\nThreat model\n\nPer 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.\n\nImpact 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.\n\nProof of Concept\n\nReproduced 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).\n\n1. Outside the Grav webroot (one directory up), place a distinguishable \"secret\" image: a solid red 200x200 PNG, secret_outside_root.png.\n2. 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:\n![carrier](carrier.png?watermark=../secret_outside_root.png)\n3. Any anonymous visitor requests the page: GET /poc. Grav renders an \u003cimg\u003e tag pointing at a cached, public derivative URL, e.g. /images/b/2/8/2/2/b282200a65ce979377963180629babd2335212ba-carrier.png.\n4. Fetching that URL (again unauthenticated) and sampling pixels confirms the composited output contains the secret file's content:\ncorner pixel (from carrier.png):            RGB(0, 0, 255)   — blue, expected\ncenter pixel (from secret_outside_root.png): RGB(255, 0, 0)   — red, exfiltrated\n\n(Test images and the exfiltrated output are attached separately — let me know if you need them regenerated.)\n\nSuggested fix\n\n- In UniformResourceLocator::findCached()'s file:// branch, resolve the candidate path with realpath() and verify it remains inside $this-\u003ebase before returning it — mirroring the containment that already exists implicitly in the non-file branch (find()).\n- 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().\n\nSuggested severity\n\nHigh — 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.","status":"published","published":"2026-09-17T17:16:56Z","modified":"2026-09-17T17:16:57Z","cvss":[{"version":"4.0","score":8.7,"vector":"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","level":"high"}],"cwes":["CWE-22"],"updated_at":"2026-11-02T14:41:18.357514Z"}],"nvd":[{"id":"CVE-2026-69089","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.","status":"published","published":"2026-08-03T14:16:29.42Z","modified":"2026-10-23T21:10:00.157Z","cvss":[{"version":"4.0","score":8.7,"vector":"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","level":"high","assessor":"secondary"},{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N","level":"high","assessor":"secondary"}],"cwes":["CWE-22"],"updated_at":"2026-11-02T14:12:34.643497Z"}],"osv":[{"id":"CVE-2026-69089","title":"Grav CMS before 2.0.11 Path Traversal via watermark","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.","status":"published","published":"2026-08-03T13:20:43.593Z","modified":"2026-10-28T02:31:04.05794142Z","cvss":[{"version":"4.0","score":8.7,"vector":"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","level":"high"}],"cwes":["CWE-22"],"updated_at":"2026-11-02T15:45:15.164658Z"},{"id":"GHSA-mmwh-j75q-gxp8","title":"Duplicate Advisory: Grav: Path Traversal in ImageMedium::watermark() — arbitrary file disclosure via publicly-cached images","description":"### Duplicate Advisory\nThis advisory has been withdrawn because it is a duplicate of GHSA-w3f4-8pj2-599w. This link is maintained to preserve external references.\n\n### Original Description\nGrav 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.","status":"withdrawn","published":"2026-08-03T15:32:48Z","modified":"2026-09-17T17:30:05.670757962Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N","level":"high"},{"version":"4.0","score":8.7,"vector":"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","level":"high"}],"cwes":["CWE-22"],"updated_at":"2026-11-02T15:46:52.170928Z"},{"id":"GHSA-w3f4-8pj2-599w","title":"Grav: Path Traversal in ImageMedium::watermark() — arbitrary file disclosure via publicly-cached images","description":"Reported by: Nihad Huseynli (@nihaddhuseynli (https://github.com/nihaddhuseynli)) — nihadd.huseynli@gmail.com\n\n▎ 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.\n\nPath Traversal in ImageMedium::watermark() leading to arbitrary file disclosure via publicly-served images\n\nSummary\n\nThe 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.\n\nAffected version\n\n- Grav CMS, develop/2.0 line, commit db8c1fcd63aaaf6d6b244bc6b4cfa5f7b96bbc7f (tip of 2.0.11 post-release).\n- Root cause lives in the pinned dependency rockettheme/toolbox v2.x-dev @ c569a53304cd7d95ff21bffa6fc590adcf0be83d (per composer.lock), specifically RocketTheme\\Toolbox\\ResourceLocator\\UniformResourceLocator.\n- 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).\n\nRoot cause\n\nUniformResourceLocator::normalize() (ResourceLocator/src/UniformResourceLocator.php:261) cleans ../. segments purely as string manipulation against $this-\u003ebase:\n\nforeach ($parts as $i =\u003e $part) {\n    if ($part === '..') {\n        $part = array_pop($list);\n        if ($part === null || $part === '' || (!$list \u0026\u0026 strpos($part, ':'))) {\n            return false; // only refuses once popped past the leading sentinel\n        }\n    } ...\n}\n\nGiven enough ../ segments to match the depth of $this-\u003ebase, 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:\n\nif ($scheme === 'file') {\n    if (!$all \u0026\u0026 !file_exists($file)) {\n        $this-\u003ecache[$key] = $array ? [] : false;\n    } else {\n        $this-\u003ecache[$key] = $array ? [$file] : $file;   // \u003c-- returned as-is\n    }\n}\n\nUnlike the else branch (find()), which re-glues resolved filenames onto a registered scheme root, the file:// branch performs no containment check.\n\nImageMedium::watermark() (system/src/Grav/Common/Page/Medium/ImageMedium.php:367) feeds attacker-influenced input straight into this resolver:\n\npublic function watermark($image = null, $position = null, $scale = null)\n{\n    ...\n    $args = func_get_args();\n    $file = $args[0] ?? '1';\n    $file = $file === '1' ? $config-\u003eget('system.images.watermark.image') : $args[0];\n\n    $watermark = $locator-\u003efindResource($file);   // no path validation\n    $watermark = ImageFile::open($watermark);      // decoded \u0026 composited\n    ...\n}\n\nwatermark 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.\n\nThreat model\n\nPer 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.\n\nImpact 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.\n\nProof of Concept\n\nReproduced 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).\n\n1. Outside the Grav webroot (one directory up), place a distinguishable \"secret\" image: a solid red 200x200 PNG, secret_outside_root.png.\n2. 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:\n![carrier](carrier.png?watermark=../secret_outside_root.png)\n3. Any anonymous visitor requests the page: GET /poc. Grav renders an \u003cimg\u003e tag pointing at a cached, public derivative URL, e.g. /images/b/2/8/2/2/b282200a65ce979377963180629babd2335212ba-carrier.png.\n4. Fetching that URL (again unauthenticated) and sampling pixels confirms the composited output contains the secret file's content:\ncorner pixel (from carrier.png):            RGB(0, 0, 255)   — blue, expected\ncenter pixel (from secret_outside_root.png): RGB(255, 0, 0)   — red, exfiltrated\n\n(Test images and the exfiltrated output are attached separately — let me know if you need them regenerated.)\n\nSuggested fix\n\n- In UniformResourceLocator::findCached()'s file:// branch, resolve the candidate path with realpath() and verify it remains inside $this-\u003ebase before returning it — mirroring the containment that already exists implicitly in the non-file branch (find()).\n- 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().\n\nSuggested severity\n\nHigh — 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.","status":"published","published":"2026-09-17T17:16:56Z","modified":"2026-09-17T17:30:05.672720563Z","cvss":[{"version":"4.0","score":8.7,"vector":"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","level":"high"}],"cwes":["CWE-22"],"updated_at":"2026-11-02T15:46:58.04805Z"}]}}
