CVE-2026-72702: Grav Origin Validation Bypass Vulnerability

LOW
Advisory Identifier: GHSA-9ccq-2jfg-qw33
CWE-346
Published
Advisory Summary

CVE-2026-72702 is a LOW origin validation bypass vulnerability (CWE-346: Origin Validation Error) affecting Grav libraries in the Packagist ecosystem. The issue involves origin validation bypass in uri::referrer() and pages::referrerroute() via unanchored prefix match. Remediation is available by upgrading affected packages to version 2.0.16.

CVE-2026-72702 at a Glance

CVE Identifier CVE-2026-72702
GHSA Identifier GHSA-9ccq-2jfg-qw33
Severity LOW
CVSS v3.1 Base Score N/A
CWE Classification CWE-346: Origin Validation Error
Affected Product Grav
Package Ecosystem Packagist
Published Date
Last Updated

How to Fix CVE-2026-72702

To remediate this vulnerability, upgrade all affected Grav dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

getgrav/grav fixed: 2.0.16
Upgrade getgrav/grav to 2.0.16
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for getgrav/grav

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-72702, inspect your project dependencies for Grav:

inspect dependencies for getgrav/grav

Analyze the terminal output to locate any version of Grav in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 2.0.16), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-346: Origin Validation Error, where the product does not properly verify that the source of data or communication is valid. In Grav, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## Summary `Grav\Common\Uri::referrer()` and `Grav\Common\Page\Pages::referrerRoute()` both check whether an incoming request's `Referer` header "came from our site" using `str_starts_with($referrer, $base)`, where `$base` is the site's own absolute root URL (for example `https://example.com`, no trailing slash). Because the comparison has no boundary character after the prefix, any `Referer` value that merely starts with that string is accepted, including a `Referer` from a completely different host such as `https://example.com.attacker.tld`. This is the same class of bug already fixed once in 2.0.15 for the fast static asset server (GHSA-4v9q-p283-qc2m, "also allowing any neighbouring directory whose name starts with the same letters"). The identical pattern is still present in both places that trust the `Referer` header, and neither is covered by that fix. ## Affected product and version Product: Grav CMS, getgrav/grav Confirmed present in: 2.0.15, commit c2b46866857a93a0aa7048e7ed707ed3ed45dbc3 The pattern is not touched by any of the 2.0.15 security fixes, so earlier 2.x releases are likely affected too. I have not checked how far back it goes. ## Affected code `system/src/Grav/Common/Uri.php`, method `referrer()`: ```php $referrer = $_SERVER['HTTP_REFERER'] ?? null; ... $base = $this->rootUrl(true); // e.g. "https://example.com", no trailing slash // Referrer should always have host set and it should come from the same base address. if (!is_string($referrer) || !str_starts_with($referrer, $base)) { $referrer = $default ?: $this->route(true, true); } $referrer = substr($referrer, strlen($base)); ``` `system/src/Grav/Common/Page/Pages.php`, method `referrerRoute()`: ```php $referrer = $_SERVER['HTTP_REFERER'] ?? null; $root = $this->grav['base_url_absolute']; // e.g. "https://example.com" if (!is_string($referrer) || !str_starts_with($referrer, (string) $root)) { return null; } ``` Note that the inner per-language loop later in the same `referrerRoute()` method does anchor the check correctly (`str_starts_with($referrer, "{$base}/")`), and `system/src/Grav/Common/Themes.php` line 300 does the same thing correctly (`$current === $base || str_starts_with($current, $base . '/')`). So the codebase already has the correct pattern elsewhere. Only the two outer checks quoted above compare against the bare root URL with no trailing delimiter. ## Root cause `str_starts_with($referrer, $base)` treats `$base` as a plain string prefix. Since `$base` has no trailing `/`, a string is accepted as long as it begins with those exact characters, regardless of what character follows. An attacker fully controls their own domain name, so producing a string that begins with the victim's origin is trivial, for example by registering `example.com.attacker.tld` or `example.com-attacker.tld`. Under the default browser Referrer Policy (`strict-origin-when-cross-origin`), a cross-origin click or form submission from the attacker's page sends only the origin (`scheme://host`) as `Referer`, which is exactly the granularity `$base` is compared at, so no unusual browser configuration is required. ## Proof of concept, verified, real output This was run directly against the actual, unmodified source file from the repository, not a reimplementation. Steps and exact output below. Step 1, clone the repo and confirm the commit under test: ``` $ git clone --depth 1 https://github.com/getgrav/grav.git $ cd grav && git log -1 --format="%H %ai" c2b46866857a93a0aa7048e7ed707ed3ed45dbc3 2026-08-03 15:14:50 +0100 ``` Step 2, install PHP to execute the real class: ``` $ apt-get install -y php-cli $ php -v PHP 8.3.6 (cli) (built: Jul 16 2026 18:30:41) (NTS) ``` Step 3, PoC harness. Full site bootstrap, composer install, database, config, is not required to demonstrate this specific bug, since `referrer()` only needs the `$root` property, which `init()` would normally compute from the site config. The harness sets that one property with PHP Reflection, then calls the real, unmodified `referrer()` method with a real `$_SERVER['HTTP_REFERER']` value, exactly the input path a live server would use: ```php <?php // poc.php spl_autoload_register(function ($class) { if (strpos($class, 'Grav\\') === 0) { $rel = str_replace('Grav\\', '', $class); $path = '/home/claude/grav/system/src/Grav/' . str_replace('\\', '/', $rel) . '.php'; if (file_exists($path)) { require_once $path; } } }); $env = [ 'HTTP_HOST' => 'example.com', 'REQUEST_URI' => '/target-route', 'HTTPS' => 'on', ]; $uri = new \Grav\Common\Uri($env); $ref = new ReflectionObject($uri); $prop = $ref->getProperty('root'); $prop->setAccessible(true); $prop->setValue($uri, 'https://example.com'); function test($label, $refererHeader) { global $uri; $_SERVER['HTTP_REFERER'] = $refererHeader; $result = $uri->referrer('https://example.com/DEFAULT_FALLBACK_USED'); echo "$label\n"; echo " Referer sent : $refererHeader\n"; echo " referrer() returned : $result\n"; echo " Same-origin check : " . ($result === '/DEFAULT_FALLBACK_USED' ? 'REJECTED (fallback used, correct)' : 'ACCEPTED (Referer treated as same-origin)') . "\n\n"; } echo "=== Grav\\Common\\Uri::referrer() executed against real, unmodified source ===\n"; echo "Site base (\$root, as init() would set it) = https://example.com\n\n"; test('[1] Legitimate same-site referrer', 'https://example.com/some/page'); test('[2] Unrelated attacker site, sanity check, must be rejected', 'https://attacker.tld/phish'); test('[3] Attacker domain string-prefixing victim domain, vulnerable case', 'https://example.com.attacker.tld/phish'); test('[4] Attacker domain, dash variant, vulnerable case', 'https://example.com-attacker.tld/phish'); ``` Step 4, run it: ``` $ php poc.php ``` Actual output: ``` === Grav\Common\Uri::referrer() executed against real, unmodified source === Site base ($root, as init() would set it) = https://example.com [1] Legitimate same-site referrer Referer sent : https://example.com/some/page referrer() returned : /some/page Same-origin check : ACCEPTED (Referer treated as same-origin) [2] Unrelated attacker site, sanity check, must be rejected Referer sent : https://attacker.tld/phish referrer() returned : /DEFAULT_FALLBACK_USED Same-origin check : REJECTED (fallback used, correct) [3] Attacker domain string-prefixing victim domain, vulnerable case Referer sent : https://example.com.attacker.tld/phish referrer() returned : .attacker.tld/phish Same-origin check : ACCEPTED (Referer treated as same-origin) [4] Attacker domain, dash variant, vulnerable case Referer sent : https://example.com-attacker.tld/phish referrer() returned : -attacker.tld/phish Same-origin check : ACCEPTED (Referer treated as same-origin) ``` Interpretation: test 2 proves the harness correctly rejects a genuinely unrelated origin, so the acceptance in tests 3 and 4 is not a harness artifact. `https://example.com.attacker.tld` and `https://example.com-attacker.tld`, both fully attacker owned and registerable domains, are treated by `referrer()` as if they were `https://example.com` itself. For a live end to end check against a running installation, this is the manual equivalent with curl once a Grav site is deployed at a known host, and it exercises the exact same `str_starts_with` comparison inside the real request path, not a standalone harness: ``` curl -s -H "Referer: https://TARGETHOST.attacker.tld/x" https://TARGETHOST/some/route ``` I did not have a fully bootstrapped live Grav instance available in this environment, composer install requires packagist.org, which was not reachable from the sandbox I was working in, so I was not able to additionally capture that live HTTP round trip. The harness above exercises the identical, unmodified vulnerable method and comparison from the real source file, so the defect itself is verified. What I could not verify from this repository alone is the specific downstream consumer of the return value, since `Pages::referrerRoute()`'s only real caller I could find references, per its own docblock example, which mentions `/admin`, is expected to live in the Admin plugin, `getgrav/grav-plugin-admin`, a separate repository not included in this checkout. If that is where a post login redirect target gets built from this value, please confirm on your end, since it would raise the severity of this report from an origin check bypass to a concrete open redirect after login. ## Impact An attacker who gets a victim to click a link, or to load a page that issues a cross-site request, from an attacker-controlled domain that string-prefixes the victim's Grav site domain can make the application treat that request as though it originated on site when it did not. The function also returns a relative route value derived directly from attacker-controlled input, via `substr($referrer, strlen($base))`, seen in test 3 and 4 above as `.attacker.tld/phish` and `-attacker.tld/phish`. If that value is later reused to build a redirect target, this becomes an open redirect. I was not able to fully confirm that chain from this repository alone, since the concrete consumer appears to live in the separate Admin plugin repository, but the origin check itself is unambiguously broken, and it is a reusable, security documented API, the docblock for `referrer()` explicitly states it checks that the referrer came from the site. ## Suggested fix Anchor the comparison the same way the codebase already does correctly elsewhere: ```php // Uri::referrer() if (!is_string($referrer) || !($referrer === $base || str_starts_with($referrer, $base . '/'))) { ... } // Pages::referrerRoute() if (!is_string($referrer) || !($referrer === $root || str_starts_with($referrer, $root . '/'))) { return null; } ``` A more robust alternative is to parse both values with `parse_url()` and compare `scheme`, `host`, and `port` as discrete fields instead of doing any string prefix comparison. ## Additional notes While reviewing this release I also checked `Utils::checkFilename()` and the `uploads_dangerous_extensions` list, the `Security::detectXss()` regex handling of `on_events` and `xmlns`, and the `twig_sandbox` allow list in `system/config/security.yaml`. All three looked solid and appear to already reflect the fixes from prior advisories, GHSA-w8cg-7jcj-4vv2, GHSA-c2q3-p4jr-c55f, GHSA-j274-39qw-32c9. I did not find further issues to report there. Given this pattern has now recurred at least three times in this codebase, the static asset server, `Uri::referrer()`, and `Pages::referrerRoute()`, it may be worth grepping for every remaining `str_starts_with($x, $base)` call site touching URLs or paths. =========================================================== CWE FIELD =========================================================== CWE-346, Origin Validation Error =========================================================== CVSS CALCULATOR SELECTIONS (v3.1) =========================================================== Attack Vector: Network Attack Complexity: Low Privileges Required: None User Interaction: Required Scope: Unchanged Confidentiality: None Integrity: Low Availability: None Resulting vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:N Resulting score: 4.3, severity Medium Note for the maintainer: this is a conservative rating for the origin check bypass on its own. If you confirm that `Pages::referrerRoute()`'s output feeds an unvalidated redirect target in the Admin plugin's post login flow, please rescore, Integrity would likely move to High and this becomes a credential phishing primitive right after a real login, which is meaningfully worse than the score above reflects. =========================================================== SEVERITY FIELD =========================================================== Moderate, pending your confirmation of the Admin plugin call site, see note above

CVE-2026-72702 Affected Grav Versions

1 component(s)
Fixed Versions:
2.0.16
Product / Package Ecosystem Affected Versions Fixed Version
getgrav/grav
Packagist
< 2.0.16
2.0.16

References

Frequently Asked Questions

What is CVE-2026-72702?
CVE-2026-72702 is a security vulnerability in Grav involving Origin validation bypass in Uri::referrer() and Pages::referrerRoute() via unanchored prefix match under CWE-346: Origin Validation Error.
Which Grav versions are affected?
Affected versions include releases prior to 2.0.16 across affected packages: getgrav/grav.
What version fixes CVE-2026-72702?
Upgrading to 2.0.16 (or the latest patched release for your respective branch) resolves the vulnerability.
Is getgrav/grav affected?
Yes, getgrav/grav is affected in releases prior to 2.0.16. Upgrade your dependency to 2.0.16 or later.
What CWE is associated with CVE-2026-72702?
CVE-2026-72702 is classified under CWE-346: Origin Validation Error (The product does not properly verify that the source of data or communication is valid.).
How can I check Packagist dependencies for CVE-2026-72702?
Run the dependency tree command: 'inspect dependencies for getgrav/grav'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-9ccq-2jfg-qw33",
  "modified" : "2026-09-17T20:24:38Z",
  "published" : "2026-09-17T20:24:38Z",
  "aliases" : [ "CVE-2026-72702" ],
  "summary" : "Grav: Origin validation bypass in Uri::referrer() and Pages::referrerRoute() via unanchored prefix match",
  "details" : "## Summary\n\n`Grav\\Common\\Uri::referrer()` and `Grav\\Common\\Page\\Pages::referrerRoute()` both check whether an incoming request's `Referer` header \"came from our site\" using `str_starts_with($referrer, $base)`, where `$base` is the site's own absolute root URL (for example `https://example.com`, no trailing slash). Because the comparison has no boundary character after the prefix, any `Referer` value that merely starts with that string is accepted, including a `Referer` from a completely different host such as `https://example.com.attacker.tld`.\n\nThis is the same class of bug already fixed once in 2.0.15 for the fast static asset server (GHSA-4v9q-p283-qc2m, \"also allowing any neighbouring directory whose name starts with the same letters\"). The identical pattern is still present in both places that trust the `Referer` header, and neither is covered by that fix.\n\n## Affected product and version\n\nProduct: Grav CMS, getgrav/grav\nConfirmed present in: 2.0.15, commit c2b46866857a93a0aa7048e7ed707ed3ed45dbc3\nThe pattern is not touched by any of the 2.0.15 security fixes, so earlier 2.x releases are likely affected too. I have not checked how far back it goes.\n\n## Affected code\n\n`system/src/Grav/Common/Uri.php`, method `referrer()`:\n```php\n$referrer = $_SERVER['HTTP_REFERER'] ?? null;\n...\n$base = $this->rootUrl(true);   // e.g. \"https://example.com\", no trailing slash\n// Referrer should always have host set and it should come from the same base address.\nif (!is_string($referrer) || !str_starts_with($referrer, $base)) {\n    $referrer = $default ?: $this->route(true, true);\n}\n$referrer = substr($referrer, strlen($base));\n```\n\n`system/src/Grav/Common/Page/Pages.php`, method `referrerRoute()`:\n```php\n$referrer = $_SERVER['HTTP_REFERER'] ?? null;\n$root = $this->grav['base_url_absolute'];   // e.g. \"https://example.com\"\nif (!is_string($referrer) || !str_starts_with($referrer, (string) $root)) {\n    return null;\n}\n```\n\nNote that the inner per-language loop later in the same `referrerRoute()` method does anchor the check correctly (`str_starts_with($referrer, \"{$base}/\")`), and `system/src/Grav/Common/Themes.php` line 300 does the same thing correctly (`$current === $base || str_starts_with($current, $base . '/')`). So the codebase already has the correct pattern elsewhere. Only the two outer checks quoted above compare against the bare root URL with no trailing delimiter.\n\n## Root cause\n\n`str_starts_with($referrer, $base)` treats `$base` as a plain string prefix. Since `$base` has no trailing `/`, a string is accepted as long as it begins with those exact characters, regardless of what character follows. An attacker fully controls their own domain name, so producing a string that begins with the victim's origin is trivial, for example by registering `example.com.attacker.tld` or `example.com-attacker.tld`.\n\nUnder the default browser Referrer Policy (`strict-origin-when-cross-origin`), a cross-origin click or form submission from the attacker's page sends only the origin (`scheme://host`) as `Referer`, which is exactly the granularity `$base` is compared at, so no unusual browser configuration is required.\n\n## Proof of concept, verified, real output\n\nThis was run directly against the actual, unmodified source file from the repository, not a reimplementation. Steps and exact output below.\n\nStep 1, clone the repo and confirm the commit under test:\n```\n$ git clone --depth 1 https://github.com/getgrav/grav.git\n$ cd grav && git log -1 --format=\"%H %ai\"\nc2b46866857a93a0aa7048e7ed707ed3ed45dbc3 2026-08-03 15:14:50 +0100\n```\n\nStep 2, install PHP to execute the real class:\n```\n$ apt-get install -y php-cli\n$ php -v\nPHP 8.3.6 (cli) (built: Jul 16 2026 18:30:41) (NTS)\n```\n\nStep 3, PoC harness. Full site bootstrap, composer install, database, config, is not required to demonstrate this specific bug, since `referrer()` only needs the `$root` property, which `init()` would normally compute from the site config. The harness sets that one property with PHP Reflection, then calls the real, unmodified `referrer()` method with a real `$_SERVER['HTTP_REFERER']` value, exactly the input path a live server would use:\n\n```php\n<?php\n// poc.php\nspl_autoload_register(function ($class) {\n    if (strpos($class, 'Grav\\\\') === 0) {\n        $rel = str_replace('Grav\\\\', '', $class);\n        $path = '/home/claude/grav/system/src/Grav/' . str_replace('\\\\', '/', $rel) . '.php';\n        if (file_exists($path)) {\n            require_once $path;\n        }\n    }\n});\n\n$env = [\n    'HTTP_HOST'   => 'example.com',\n    'REQUEST_URI' => '/target-route',\n    'HTTPS'       => 'on',\n];\n\n$uri = new \\Grav\\Common\\Uri($env);\n\n$ref = new ReflectionObject($uri);\n$prop = $ref->getProperty('root');\n$prop->setAccessible(true);\n$prop->setValue($uri, 'https://example.com');\n\nfunction test($label, $refererHeader) {\n    global $uri;\n    $_SERVER['HTTP_REFERER'] = $refererHeader;\n    $result = $uri->referrer('https://example.com/DEFAULT_FALLBACK_USED');\n    echo \"$label\\n\";\n    echo \"  Referer sent        : $refererHeader\\n\";\n    echo \"  referrer() returned : $result\\n\";\n    echo \"  Same-origin check   : \" . ($result === '/DEFAULT_FALLBACK_USED' ? 'REJECTED (fallback used, correct)' : 'ACCEPTED (Referer treated as same-origin)') . \"\\n\\n\";\n}\n\necho \"=== Grav\\\\Common\\\\Uri::referrer() executed against real, unmodified source ===\\n\";\necho \"Site base (\\$root, as init() would set it) = https://example.com\\n\\n\";\n\ntest('[1] Legitimate same-site referrer', 'https://example.com/some/page');\ntest('[2] Unrelated attacker site, sanity check, must be rejected', 'https://attacker.tld/phish');\ntest('[3] Attacker domain string-prefixing victim domain, vulnerable case', 'https://example.com.attacker.tld/phish');\ntest('[4] Attacker domain, dash variant, vulnerable case', 'https://example.com-attacker.tld/phish');\n```\n\nStep 4, run it:\n```\n$ php poc.php\n```\n\nActual output:\n```\n=== Grav\\Common\\Uri::referrer() executed against real, unmodified source ===\nSite base ($root, as init() would set it) = https://example.com\n\n[1] Legitimate same-site referrer\n  Referer sent        : https://example.com/some/page\n  referrer() returned : /some/page\n  Same-origin check   : ACCEPTED (Referer treated as same-origin)\n\n[2] Unrelated attacker site, sanity check, must be rejected\n  Referer sent        : https://attacker.tld/phish\n  referrer() returned : /DEFAULT_FALLBACK_USED\n  Same-origin check   : REJECTED (fallback used, correct)\n\n[3] Attacker domain string-prefixing victim domain, vulnerable case\n  Referer sent        : https://example.com.attacker.tld/phish\n  referrer() returned : .attacker.tld/phish\n  Same-origin check   : ACCEPTED (Referer treated as same-origin)\n\n[4] Attacker domain, dash variant, vulnerable case\n  Referer sent        : https://example.com-attacker.tld/phish\n  referrer() returned : -attacker.tld/phish\n  Same-origin check   : ACCEPTED (Referer treated as same-origin)\n```\n\nInterpretation: test 2 proves the harness correctly rejects a genuinely unrelated origin, so the acceptance in tests 3 and 4 is not a harness artifact. `https://example.com.attacker.tld` and `https://example.com-attacker.tld`, both fully attacker owned and registerable domains, are treated by `referrer()` as if they were `https://example.com` itself.\n\nFor a live end to end check against a running installation, this is the manual equivalent with curl once a Grav site is deployed at a known host, and it exercises the exact same `str_starts_with` comparison inside the real request path, not a standalone harness:\n```\ncurl -s -H \"Referer: https://TARGETHOST.attacker.tld/x\" https://TARGETHOST/some/route\n```\nI did not have a fully bootstrapped live Grav instance available in this environment, composer install requires packagist.org, which was not reachable from the sandbox I was working in, so I was not able to additionally capture that live HTTP round trip. The harness above exercises the identical, unmodified vulnerable method and comparison from the real source file, so the defect itself is verified. What I could not verify from this repository alone is the specific downstream consumer of the return value, since `Pages::referrerRoute()`'s only real caller I could find references, per its own docblock example, which mentions `/admin`, is expected to live in the Admin plugin, `getgrav/grav-plugin-admin`, a separate repository not included in this checkout. If that is where a post login redirect target gets built from this value, please confirm on your end, since it would raise the severity of this report from an origin check bypass to a concrete open redirect after login.\n\n## Impact\n\nAn attacker who gets a victim to click a link, or to load a page that issues a cross-site request, from an attacker-controlled domain that string-prefixes the victim's Grav site domain can make the application treat that request as though it originated on site when it did not. The function also returns a relative route value derived directly from attacker-controlled input, via `substr($referrer, strlen($base))`, seen in test 3 and 4 above as `.attacker.tld/phish` and `-attacker.tld/phish`. If that value is later reused to build a redirect target, this becomes an open redirect. I was not able to fully confirm that chain from this repository alone, since the concrete consumer appears to live in the separate Admin plugin repository, but the origin check itself is unambiguously broken, and it is a reusable, security documented API, the docblock for `referrer()` explicitly states it checks that the referrer came from the site.\n\n## Suggested fix\n\nAnchor the comparison the same way the codebase already does correctly elsewhere:\n```php\n// Uri::referrer()\nif (!is_string($referrer) || !($referrer === $base || str_starts_with($referrer, $base . '/'))) { ... }\n\n// Pages::referrerRoute()\nif (!is_string($referrer) || !($referrer === $root || str_starts_with($referrer, $root . '/'))) { return null; }\n```\nA more robust alternative is to parse both values with `parse_url()` and compare `scheme`, `host`, and `port` as discrete fields instead of doing any string prefix comparison.\n\n## Additional notes\n\nWhile reviewing this release I also checked `Utils::checkFilename()` and the `uploads_dangerous_extensions` list, the `Security::detectXss()` regex handling of `on_events` and `xmlns`, and the `twig_sandbox` allow list in `system/config/security.yaml`. All three looked solid and appear to already reflect the fixes from prior advisories, GHSA-w8cg-7jcj-4vv2, GHSA-c2q3-p4jr-c55f, GHSA-j274-39qw-32c9. I did not find further issues to report there. Given this pattern has now recurred at least three times in this codebase, the static asset server, `Uri::referrer()`, and `Pages::referrerRoute()`, it may be worth grepping for every remaining `str_starts_with($x, $base)` call site touching URLs or paths.\n\n===========================================================\nCWE FIELD\n===========================================================\nCWE-346, Origin Validation Error\n\n===========================================================\nCVSS CALCULATOR SELECTIONS (v3.1)\n===========================================================\nAttack Vector: Network\nAttack Complexity: Low\nPrivileges Required: None\nUser Interaction: Required\nScope: Unchanged\nConfidentiality: None\nIntegrity: Low\nAvailability: None\n\nResulting vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:N\nResulting score: 4.3, severity Medium\n\nNote for the maintainer: this is a conservative rating for the origin check bypass on its own. If you confirm that `Pages::referrerRoute()`'s output feeds an unvalidated redirect target in the Admin plugin's post login flow, please rescore, Integrity would likely move to High and this becomes a credential phishing primitive right after a real login, which is meaningfully worse than the score above reflects.\n\n===========================================================\nSEVERITY FIELD\n===========================================================\nModerate, pending your confirmation of the Admin plugin call site, see note above",
  "severity" : [ ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Packagist",
      "name" : "getgrav/grav"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "2.0.16"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 2.0.15"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/getgrav/grav/security/advisories/GHSA-9ccq-2jfg-qw33"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-72702"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/getgrav/grav"
  }, {
    "type" : "WEB",
    "url" : "https://www.vulncheck.com/advisories/grav-cms-before-origin-validation-bypass-via-referer"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-346" ],
    "severity" : "LOW",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-09-17T20:24:38Z",
    "nvd_published_at" : null
  }
}