Skip to main content
CVE Vulnerability Database
Vulnerability Database/CVE-2026-69246

CVE-2026-69246: Guzzle PHP HTTP Client SSRF Vulnerability

CVE-2026-69246 is a server-side request forgery flaw in Guzzle PHP HTTP client that allows attackers to bypass host checks and reach restricted servers. This post explains its impact, affected versions, and mitigation.

Published:

CVE-2026-69246 Overview

CVE-2026-69246 is a host validation flaw in Guzzle, an extensible PHP HTTP client library. The vulnerability affects Guzzle versions prior to 7.15.2 and 8.0.1. It stems from a divergence between how Guzzle parses a request URI host and how the underlying transport (libcurl or fopen()) resolves that host after percent-decoding and IDNA mapping. An attacker who influences a fetched URI can bypass application-level host checks and reach unintended destinations, including internal or loopback services. The issue is tracked under [CWE-180: Incorrect Behavior Order: Validate Before Canonicalize].

Critical Impact

Attackers who control a request URI can bypass host allow-list checks, reach internal endpoints such as 127.0.0.1, and force incorrect proxy or Authorization/Cookie handling decisions.

Affected Products

  • Guzzle HTTP client versions prior to 7.15.2
  • Guzzle HTTP client versions prior to 8.0.1
  • PHP applications that construct request URIs from untrusted input using Guzzle

Discovery Timeline

  • 2026-08-03 - CVE-2026-69246 published to NVD
  • 2026-08-04 - Last updated in NVD database

Technical Details for CVE-2026-69246

Vulnerability Analysis

Guzzle hands a transport the request URI as text and supplies the Host header separately. The cURL handler sets CURLOPT_URL to the URI as written and pushes the Host into CURLOPT_HTTPHEADER. The StreamHandler performs the equivalent through fopen(). libcurl then parses the authority itself, percent-decodes it, and applies IDNA mapping on IDN-capable builds. The result is used to resolve DNS, open the TCP connection, name the TLS peer, and address a proxy CONNECT. The application-supplied Host header suppresses the aligned one libcurl would have generated. This creates a split view of the destination between application checks and the transport.

Root Cause

The root cause is a validate-before-canonicalize ordering flaw. Guzzle and calling code inspect the literal host string, but libcurl and fopen() operate on a canonicalized form. For a URI host written as 127.0.0.%31, filter_var() rejects the host as an IP literal. libcurl decodes it to 127.0.0.1 and reaches loopback with no DNS lookup, while the server receives Host: 127.0.0.%31. The same divergence affects Guzzle's own decisions: no_proxy selects proxy routing from the literal host, and RedirectMiddleware uses it to decide whether to strip Authorization and Cookie headers.

Attack Vector

Exploitation requires the application to build a request URI from untrusted input and to make a host decision before handing it to Guzzle. An attacker submits a URI whose host contains percent-encoded or IDNA-mappable characters that pass application allow-list checks but decode to an internal address after libcurl parses the URI. The result is a server-side request forgery (SSRF) class outcome, where the response body from an internal host is returned to the attacker.

php
// Security patch in src/Client.php - Security fixes 8.0 (#3908)
        $options = $this->prepareDefaults($options);

        return $this->transfer(
-            $request->withUri($this->buildUri($request->getUri(), $options), $request->hasHeader('Host')),
+            $request->withUri($this->buildUri($request->getUri(), $options), self::shouldPreserveHost($request)),
            $options
        );
    }
// Source: https://github.com/guzzle/guzzle/commit/3aeea0406aab88cbbd86531313d7cebf8ae149a4
php
// Security patch in src/Handler/CurlHandler.php - adds HostValidator (#3908)
        array $options
    ): PromiseInterface {
        $this->assertOpen();
+        HostValidator::assertRequestHost($request);

        if (isset($options['delay'])) {
            \usleep((int) ($options['delay'] * 1000));
// Source: https://github.com/guzzle/guzzle/commit/3aeea0406aab88cbbd86531313d7cebf8ae149a4

The fix introduces HostValidator::assertRequestHost() to reject URIs whose literal host would not match the canonicalized form the transport actually uses.

Detection Methods for CVE-2026-69246

Indicators of Compromise

  • Outbound HTTP requests where the Host header contains percent-encoded characters (for example Host: 127.0.0.%31).
  • Application logs showing successful fetches of URIs whose host was intended to be blocked by an allow-list.
  • Connections from application servers to loopback, link-local (169.254.0.0/16), or RFC1918 addresses that do not match audited egress patterns.
  • Unexpected proxy CONNECT targets or bypasses of no_proxy policy on outbound traffic.

Detection Strategies

  • Inventory PHP applications and identify Guzzle versions in use via composer.lock; flag any version below 7.15.2 or 8.0.1.
  • Inspect outbound HTTP traffic from application tiers for Host header values containing % characters or non-ASCII IDNA-mappable input.
  • Correlate application access logs with egress connection logs to detect divergence between the URI host recorded in code and the resolved destination.

Monitoring Recommendations

  • Alert on any outbound HTTP request whose Host header does not match a canonical resolved destination.
  • Monitor for application-tier connections to internal metadata endpoints such as 169.254.169.254 or loopback listeners.
  • Log Guzzle exceptions raised by HostValidator::assertRequestHost() after patching to identify prior probing attempts.

How to Mitigate CVE-2026-69246

Immediate Actions Required

  • Upgrade Guzzle to version 7.15.2 or 8.0.1 via composer update guzzlehttp/guzzle.
  • Audit application code for URI construction from untrusted input and any host-based allow-list checks performed before handing the URI to Guzzle.
  • Restrict egress from application servers so that internal services and cloud metadata endpoints are not reachable from HTTP client contexts.

Patch Information

The maintainers released fixes in Guzzle 7.15.2 and Guzzle 8.0.1. The patches are described in Pull Request #3907 and Pull Request #3908, and are documented in GitHub Security Advisory GHSA-v5mv-p594-2x33. The fix introduces a HostValidator that asserts the request host is safe before it reaches the cURL or stream handler.

Workarounds

  • Reject any user-supplied URI whose host contains a % character or non-ASCII characters before passing it to Guzzle.
  • Perform host validation only after canonicalizing the URI with the same rules libcurl applies (percent-decoding and IDNA mapping).
  • Enforce egress network controls that block application servers from reaching loopback, link-local, and internal ranges regardless of URI content.
bash
# Update Guzzle to a patched release
composer require guzzlehttp/guzzle:^8.0.1
# Or for the 7.x branch
composer require guzzlehttp/guzzle:^7.15.2

# Verify installed version
composer show guzzlehttp/guzzle | grep versions

Disclaimer: This content was generated using AI. While we strive for accuracy, please verify critical information with official sources.

Default Legacy - Prefooter | Experience the World’s Most Advanced Cybersecurity Platform

Experience the Most Advanced Cybersecurity Platform

See how the world’s most intelligent, autonomous cybersecurity platform can protect your organization today and into the future.