VulnWatch VulnWatch
← Back to dashboard
Low github · GHSA-9ccq-2jfg-qw33

Grav: Origin validation bypass in Uri::referrer() and Pages::referrerRoute() via unanchored prefix match

Published Sep 17, 2026 CVSS 0.0

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():

$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():

$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:

Affected AI Products

claude
Get the weekly digest. Every Monday: top AI security stories of the week. Free.