Aug 4, 2026·7 min read·2 visits
Host validation bypass in Guzzle allows SSRF and proxy routing bypass by exploiting differences in how Guzzle and underlying transport libraries parse noncanonical URI hostnames.
CVE-2026-69246 is a host validation bypass vulnerability in the Guzzle PHP HTTP client. The flaw resides in Guzzle's core HTTP transport handlers (cURL and PHP stream wrappers). Under specific conditions, a parser differential occurs between the host validation layer and the underlying network transport library (e.g., libcurl), allowing remote attackers to bypass SSRF filters, proxy routing rules, and redirect protections via crafted noncanonical URI representations.
Guzzle is an extensible PHP HTTP client used extensively across the PHP ecosystem to transmit asynchronous and synchronous HTTP requests. It acts as an abstraction layer over low-level network operations, wrapping PHP's native streams or the cURL extension (ext-curl). Because Guzzle simplifies remote service interactions, applications frequently rely on it to fetch resources, integrate APIs, and process user-supplied URLs.
The vulnerability designated as CVE-2026-69246 arises from a parser differential between the application-level validation logic, Guzzle's internal host identification, and the underlying transport mechanisms. Specifically, when handling noncanonical URIs, Guzzle's cURL-based handlers and PHP stream wrappers process host configurations differently from the underlying OS-level network libraries. This discrepancy introduces a security risk, primary among which is Server-Side Request Forgery (SSRF).
By crafting noncanonical URI host representations—such as using percent-encoding or IP literals with trailing root dots—unauthenticated remote attackers can bypass host-based security checks. These checks include custom application SSRF filters, Guzzle's proxy routing exclusions, and redirect credential-stripping mechanisms. This analysis dissects the technical mechanics, the code implementation of the patch, and the remaining threat vectors.
The fundamental breakdown in CVE-2026-69246 lies in the sequence of input processing operations: validation is performed before canonicalization (CWE-180). When Guzzle prepares to transmit an HTTP request, it validates and sanitizes the target URI string according to standard application filters or PSR-7 guidelines. However, when the client passes the raw host string to low-level transport engines such as libcurl, those engines execute their own internal canonicalization processes.
For instance, when cURL receives a URI host containing percent-escaped octets (such as %31 representing the character 1), it decodes these octets prior to executing the DNS resolution or establishing the socket connection. An application trying to block requests to the local loopback adapter 127.0.0.1 might inspect the string 127.0.0.%31 and conclude it is an external, non-blacklisted domain name. Once passed to libcurl, the string is canonicalized to 127.0.0.1, bypassing the control entirely.
A similar interpretation conflict (CWE-436) occurs with IP literals that include a trailing root dot, such as 127.0.0.1. Naive string parsers or regular expressions fail to match this format against standard IP address patterns, treating it instead as a fully qualified domain name (FQDN). Conversely, libcurl normalizes the string by stripping the trailing dot and processes it as the numeric loopback IP. Guzzle's internal state machine remains unaware of this transformation, causing incorrect routing, proxy, and cookie security decisions.
To understand the exact sequence of events, we can trace the path of a noncanonical request through Guzzle's validation and transport layers. The architectural difference in string parsing between Guzzle's validation point and libcurl's execution point creates the exploitable state.
The diagram below outlines how the unvalidated noncanonical URI string circumvents local checks to communicate with internal network environments.
Prior to the implementation of the security patches, Guzzle's transport handlers accepted any arbitrary host string configured in the request URI. In both cURL and stream-based handlers, the client passed the request unmodified. The following segment illustrates how the vulnerability was addressed in the patch committed to Guzzle's 8.x branch.
// Guzzle Http Handler code after the security patch
namespace GuzzleHttp\Handler;
use GuzzleHttp\Exception\RequestException;
use Psr\Http\Message\RequestInterface;
final class HostValidator
{
public static function assertRequestHost(RequestInterface $request): void
{
// Validate both the URI host and the explicit Host headers
self::assertUriHost($request->getUri()->getHost(), $request);
foreach ($request->getHeader('Host') as $value) {
self::assertPrintableAscii(
(string) $value,
'The request Host header "%s" must contain only printable ASCII.',
$request
);
}
}
private static function assertUriHost(string $host, RequestInterface $request): void
{
// Enforce printable ASCII to block non-standard representations
self::assertPrintableAscii($host, '...', $request);
// Check for percent-encoding in the host component (CWE-180 prevention)
if (\strpos($host, '%') !== false) {
throw new RequestException(\sprintf('The request URI host "%s" must not contain a percent escape.', self::escape($host)), $request);
}
// Verify compliance with standard RFC 3986 URI host structures
if (!Psr7\Rfc3986::isValidHost($host)) {
throw new RequestException(\sprintf('The request URI host "%s" must be a valid RFC 3986 host.', self::escape($host)), $request);
}
// Identify and block trailing-dot IP address formats
if (\str_ends_with($host, '.') && self::isNumericIpv4Host(\rtrim($host, '.'))) {
throw new RequestException(\sprintf('The request URI host "%s" must not be written as decimal, octal or hexadecimal parts followed by dots.', self::escape($host)), $request);
}
}
}The introduction of this HostValidator class successfully mitigates the primary parser differentials. By forcing strict conformity with RFC 3986 and rejecting the presence of percent signs in the host segment, Guzzle prevents the underlying libcurl transport engine from altering the target address post-validation. The validation executes inside the handler invocation sequence, ensuring that no request is passed to cURL or PHP stream contexts without undergoing these sanitization checks.
Exploitation of CVE-2026-69246 requires that an application exposes an endpoint that accepts user-supplied URLs or hostname strings and routes them through Guzzle. The attacker does not need any privileges or authenticated sessions. The primary attack vector targets applications implementing an IP blacklist or an intranet exclusion list.
In a typical exploitation flow, the attacker supplies a target parameter such as target_url=http://169.254.169.25%34/latest/meta-data/. The target application utilizes PHP's native parse_url() function to extract the hostname, yielding 169.254.169.25%34. Because this string does not match the exact blacklist IP string 169.254.169.254, the request is permitted.
Upon receiving this URI, Guzzle passes it to CURLOPT_URL. The underlying libcurl engine decodes %34 to 4, normalizing the destination to 169.254.169.254 (the AWS Link-Local metadata endpoint). The HTTP request is then successfully dispatched to the local network, and the response is returned to the attacker, leading to sensitive metadata exposure.
To resolve this vulnerability, organizations must upgrade Guzzle dependencies to version 7.15.2 or 8.0.1 depending on their primary major version branch. In environments where immediate dependency updates are not feasible, developers must implement robust host validation directly inside their codebases. Rather than validating raw URL strings, applications must resolve hostnames to their underlying IP addresses using gethostbyname() and validate those IPs using FILTER_VALIDATE_IP with the flags FILTER_FLAG_NO_PRIV_RANGE and FILTER_FLAG_NO_RES_RANGE.
It is critical to evaluate the completeness of the Guzzle fix. While the patch successfully addresses percent-encoding and trailing-dot IP evasion, it does not validate alternative numeric formats that do not contain percent signs or trailing dots. For example, shorthand octal IP representations like 0177.0.0.1 or hexadecimal conversions like 0x7f000001 are not explicitly blocked by the HostValidator because they conform to printable ASCII and contain no percent characters.
If an application relies solely on simple string matching (e.g., checking if the host starts with 127.0.0.1) without performing active DNS resolution, these alternate formats will still bypass the filter and resolve to the local loopback adapter. Therefore, Guzzle's patch must be paired with application-level DNS resolution validation to achieve comprehensive protection against Server-Side Request Forgery.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
Guzzle Guzzle | < 7.15.2 | 7.15.2 |
Guzzle Guzzle | >= 8.0.0, < 8.0.1 | 8.0.1 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-436 (Interpretation Conflict) |
| Attack Vector | Network |
| CVSS v3.1 | 7.2 (High) |
| Exploit Status | Proof of Concept |
| KEV Status | Not Listed |
| Impact | Server-Side Request Forgery (SSRF) / Information Disclosure |
The application handles input in a way that is interpreted differently by downstream or upstream components, leading to a breakdown in validation or security controls.
An asynchronous HTTP client/server framework for asyncio and Python, aiohttp prior to version 3.14.2 is vulnerable to HTTP Request Smuggling. The server-side HTTP parser immediately transitions the protocol state to 'upgraded' upon receiving a WebSocket upgrade request before consuming the accompanying request body. If the backend handler rejects the upgrade request while keeping the TCP connection alive, the unconsumed request body remains in the socket buffer and is parsed as a subsequent pipelined HTTP request. This allows an attacker to smuggle requests, bypass frontend reverse proxy controls, and perform unauthorized actions.
A vulnerability in the Guzzle HTTP client allows session identifiers, auth tokens, or cookies to be leaked to unauthorized hosts due to incorrect cookie domain validation of noncanonical IPv4 host formats. Guzzle failed to recognize octal, hexadecimal, and percent-encoded IP addresses as IP literals, treating them as standard domains and incorrectly extending their scope to subdomains.
A side-channel vulnerability in pyca/cryptography (versions 44.0.0 through 49.9.9) allows unauthenticated remote attackers to expose a Bleichenbacher oracle. This flaw exists within the PKCS#7 decryption module (specifically pkcs7_decrypt_der, pkcs7_decrypt_pem, and pkcs7_decrypt_smime) during Content Encryption Key (CEK) decryption when using RSA PKCS#1 v1.5 padding. Differences in error classification and symmetric execution timing allow an attacker to reconstruct plaintext keys.
An uncontrolled resource consumption vulnerability (CWE-400) exists in the python-cryptography library's Rust-based X.509 verification engine. The flaw allows unauthenticated remote attackers to trigger severe CPU exhaustion and Denial of Service (DoS) by supplying specially crafted certificate chains containing duplicate self-signed certificates, forcing the recursive path builder into an exponential state-search loop.
An improper certificate validation vulnerability (CWE-295) in the Rust-based X.509 verification engine of python-cryptography allows wildcard Subject Alternative Names (SANs) to bypass permitted Name Constraints. This enables an attacker to construct certificates that escape the restricted scope of a subordinate Certificate Authority (CA) and successfully authenticate against vulnerable client installations. The vulnerability is tracked as CVE-2026-69248 and GHSA-m2h6-j472-rp4c, with a CVSS v4.0 base score of 6.9.
A high-severity heap-based out-of-bounds (OOB) read vulnerability exists in the Cython-based HTTP response and request parser extension of aiohttp. When processing malformed HTTP traffic, the parser fails to properly handle raw C pointers returned by the underlying llhttp library during error-message construction. This triggers an uncontrolled strlen() call on non-null-terminated network buffers, which can result in a Denial of Service (DoS) via worker process crash or the exposure of adjacent heap memory inside exception messages.