CVE-2026-79065
Overview
Background
- `IsRequestHeaderSafe()`
- a helper in
services/network/public/cpp/header_util.ccthat decides whether a caller-supplied request header name/value pair may be attached to a network request; per the commit message, accepted headers are passed through to the//netstack viaMergeFrom(). - `Connection` header
- a comma-separated token list (RFC 9110 §7.6.1) in which, besides connection options like
closeandkeep-alive, each token names a header that intermediaries must strip before forwarding the message. - `kUnsafeHeaderValues`
- the removed table of (header name, exact value) pairs that
IsRequestHeaderSafe()rejected; its only entry wasConnection: Upgrade. - `net::HttpUtil::ValuesIterator`
- a
//netutility that walks a delimiter-separated header value token by token, skipping empty entries and surrounding whitespace.
Root Cause Analysis
Before the patch, IsRequestHeaderSafe() treated Connection as a “safe with exceptions” header: after the forbidden-name check, it looped over kUnsafeHeaderValues and returned false only when the value matched "Upgrade" case-insensitively as the whole string. Every other Connection value was accepted, including token lists such as keep-alive, Upgrade (which slipped past the exact match) and arbitrary header names such as X-Forwarded-For, X-Real-IP or Authorization, as the new unit-test cases show. Because Connection is a hop-by-hop list, any header name in it tells proxies and other intermediaries to drop that header before forwarding. So a caller that could only set “safe” headers could still make intermediaries delete headers the browser or the network path depends on, and could smuggle an Upgrade token past the check. The invariant that was broken is that a caller-controlled Connection value should only carry the connection-management options the network stack itself uses.
The fix makes the check stricter: it tokenizes the value with net::HttpUtil::ValuesIterator on , and returns false if any non-empty token is something other than close or keep-alive (case-insensitive). That covers the old Upgrade rule, so the table is removed. Empty or comma-only values are still accepted.
close and keep-alive.Attack Path
- Obtain a header-setting client
The attacker needs code that can hand request headers to the network service through a path validated by
IsRequestHeaderSafe(). The diff does not show which callers use it. Ordinary page script cannot setConnectionbecause Blink’s Fetch/XHR treat it as a forbidden header name, so this most plausibly needs a compromised renderer or another less-trusted client that the network service validates (inferred). - Supply a crafted `Connection` value
The client sets
Connectionto a token list naming other headers (for exampleclose, X-Real-IPorAuthorization, keep-alive), or hides anUpgradetoken inside a list (keep-alive, Upgrade). The old exact-match check accepted all of these. - Header passes validation and is merged
IsRequestHeaderSafe()returnstrue, so the value reaches the//netrequest headers viaMergeFrom()and goes out on the wire. - Intermediaries act on the token list Proxies, CDNs or reverse proxies on the path treat each listed name as hop-by-hop and strip that header before forwarding. That can remove security-relevant headers the origin relies on, such as client-IP or auth headers, or confuse upgrade handling (inferred; depends on intermediary behaviour).
Impact Assessment
Connection. That can weaken server-side controls that depend on those headers, or interfere with connection upgrade handling (inferred). The effect is on the network request and on remote infrastructure, not on memory safety in the browser. Reaching the check most likely requires a compromised renderer or another constrained client, because web content cannot set Connection directly (inferred). The vendor advisory rates this medium severity and does not report in-the-wild exploitation; NVD scores it CVSS 4.3.Changed Functions
| Function | Change | Notes |
|---|---|---|
forservices/network/public/cpp/header_util.cc |
modified |
Files Changed
services/network/public/cpp/header_util_unittest.ccservices/network/public/cpp/header_util.cc
Audit Directions
- List-valued headers checked with whole-string equalityLook in other header allow/deny lists (CORS safelisting, extension header modification,
URLLoadervalidation) for comparisons against a single literal value where the header grammar allows comma-separated lists, quoted strings or parameters. - Hop-by-hop and framing headers left on TODO listsThe remaining TODO still lists
Cookie,Date,Expect,RefererandVia. Check whether any of them (for exampleExpect: 100-continue) lets a less-trusted caller change connection or proxy behaviour. - Divergence between validator parsing and `//net` parsingMake sure validators tokenize the same way
//netand intermediaries do (whitespace, empty elements, case, duplicate headers merged byMergeFrom()), so a value that looks harmless to the check is not read differently downstream.
Patch
From 4deb569d704e2efd8dedf6df8fa4778a7b77fe8a Mon Sep 17 00:00:00 2001 From: Keita Suzuki <[email protected]> Date: Mon, 06 Jul 2026 01:02:57 -0700 Subject: [PATCH] network: Restrict Connection request-header to known tokens IsRequestHeaderSafe() previously only rejected the Connection request header when its value was the exact string "Upgrade". Connection is a comma-separated token list (RFC 9110 7.6.1) where each token names a header that intermediaries drop before forwarding, and the value is passed through to the //net stack via MergeFrom(), so callers should not be able to list arbitrary header names there. Parse the value as a token list and only accept the connection-management options the network stack itself uses ("close" and "keep-alive"). This subsumes the old exact-match "Upgrade" check, so the now-unused kUnsafeHeaderValues table is removed and Connection is dropped from the outstanding TODO. TAG=agy CONV=13ec7a8f-4d73-4c01-b1b7-4d465e984eb3 Bug: 501799770 Change-Id: I3617e93de8f283450d26ce28ee073d4bf17c3e25 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8033741 Reviewed-by: Kenichi Ishibashi <[email protected]> Commit-Queue: Keita Suzuki <[email protected]> Cr-Commit-Position: refs/heads/main@{#1657017} --- diff --git a/services/network/public/cpp/header_util.cc b/services/network/public/cpp/header_util.cc index ad6a4d9a..db12b68 100644 --- a/services/network/public/cpp/header_util.cc +++ b/services/network/public/cpp/header_util.cc @@ -45,7 +45,8 @@ net::HttpRequestHeaders::kHost, // Trailers are not supported. - "Trailer", "Te", + "Trailer", + "Te", // Websockets use a different API. "Upgrade", @@ -63,19 +64,7 @@ "Set-Cookie", // TODO(mmenke): Figure out what to do about the remaining headers: - // Connection, Cookie, Date, Expect, Referer, Via. -}; - -// Headers that consumers are currently allowed to set, with the exception of -// certain values could cause problems. -// TODO(mmenke): Gather stats on these, and see if these headers can be banned -// outright instead. -const struct { - const char* name; - const char* value; -} kUnsafeHeaderValues[] = { - // Websockets use a different API. - {net::HttpRequestHeaders::kConnection, "Upgrade"}, + // Cookie, Date, Expect, Referer, Via. }; } // namespace @@ -86,10 +75,19 @@ return false; } - for (const auto& header : kUnsafeHeaderValues) { - if (base::EqualsCaseInsensitiveASCII(header.name, key) && - base::EqualsCaseInsensitiveASCII(header.value, value)) { - return false; + // The Connection header is a comma-separated list of tokens. Per RFC 9110 + // section 7.6.1, intermediaries treat each listed token as the name of a + // header to remove before forwarding, so only allow the connection-management + // options that the network stack itself uses. Websockets use a different API, + // so "upgrade" is not needed here. + if (base::EqualsCaseInsensitiveASCII(key, + net::HttpRequestHeaders::kConnection)) { + net::HttpUtil::ValuesIterator tokens(value, ','); + while (tokens.GetNext()) { + if (!base::EqualsCaseInsensitiveASCII(tokens.value(), "close") && + !base::EqualsCaseInsensitiveASCII(tokens.value(), "keep-alive")) { + return false; + } } } diff --git a/services/network/public/cpp/header_util_unittest.cc b/services/network/public/cpp/header_util_unittest.cc index b3e43057..95c84172 100644 --- a/services/network/public/cpp/header_util_unittest.cc +++ b/services/network/public/cpp/header_util_unittest.cc @@ -30,9 +30,25 @@ {net::HttpRequestHeaders::kConnection, "Upgrade", false}, {net::HttpRequestHeaders::kConnection, "Close", true}, + {net::HttpRequestHeaders::kConnection, "keep-alive", true}, + {net::HttpRequestHeaders::kConnection, "keep-alive, close", true}, + {net::HttpRequestHeaders::kConnection, "keep-alive, Upgrade", false}, + {net::HttpRequestHeaders::kConnection, "X-Forwarded-For", false}, + {net::HttpRequestHeaders::kConnection, "close, X-Real-IP", false}, + {net::HttpRequestHeaders::kConnection, "Authorization, keep-alive", + false}, + {net::HttpRequestHeaders::kConnection, "", true}, + {net::HttpRequestHeaders::kConnection, " ", true}, + {net::HttpRequestHeaders::kConnection, ",", true}, + {net::HttpRequestHeaders::kConnection, ",,,", true}, + {net::HttpRequestHeaders::kConnection, " , , ", true}, + {net::HttpRequestHeaders::kConnection, "close, ", true}, + {net::HttpRequestHeaders::kConnection, ", keep-alive", true}, + {net::HttpRequestHeaders::kConnection, ", keep-alive, , close, ", true}, {net::HttpRequestHeaders::kTransferEncoding, "Chunked", false}, {net::HttpRequestHeaders::kTransferEncoding, "Chunky", false}, {"cOnNeCtIoN", "uPgRaDe", false}, + {"cOnNeCtIoN", "kEeP-aLiVe", true}, {net::HttpRequestHeaders::kProxyAuthorization, "Basic Zm9vOmJhcg==", false}, @@ -78,6 +94,18 @@ {net::HttpRequestHeaders::kConnection, "Upgrade", false}, {net::HttpRequestHeaders::kConnection, "Close", true}, + {net::HttpRequestHeaders::kConnection, "keep-alive", true}, + {net::HttpRequestHeaders::kConnection, "keep-alive, Upgrade", false}, + {net::HttpRequestHeaders::kConnection, "X-Forwarded-For", false}, + {net::HttpRequestHeaders::kConnection, "close, X-Real-IP", false}, + {net::HttpRequestHeaders::kConnection, "", true}, + {net::HttpRequestHeaders::kConnection, " ", true}, + {net::HttpRequestHeaders::kConnection, ",", true}, + {net::HttpRequestHeaders::kConnection, ",,,", true}, + {net::HttpRequestHeaders::kConnection, " , , ", true}, + {net::HttpRequestHeaders::kConnection, "close, ", true}, + {net::HttpRequestHeaders::kConnection, ", keep-alive", true}, + {net::HttpRequestHeaders::kConnection, ", keep-alive, , close, ", true}, {net::HttpRequestHeaders::kTransferEncoding, "Chunked", false}, {net::HttpRequestHeaders::kTransferEncoding, "Chunky", false}, {"cOnNeCtIoN", "uPgRaDe", false},
Regression Test / PoC
diff --git a/services/network/public/cpp/header_util_unittest.cc b/services/network/public/cpp/header_util_unittest.cc
index b3e43057..95c84172 100644
--- a/services/network/public/cpp/header_util_unittest.cc
+++ b/services/network/public/cpp/header_util_unittest.cc
@@ -30,9 +30,25 @@
{net::HttpRequestHeaders::kConnection, "Upgrade", false},
{net::HttpRequestHeaders::kConnection, "Close", true},
+ {net::HttpRequestHeaders::kConnection, "keep-alive", true},
+ {net::HttpRequestHeaders::kConnection, "keep-alive, close", true},
+ {net::HttpRequestHeaders::kConnection, "keep-alive, Upgrade", false},
+ {net::HttpRequestHeaders::kConnection, "X-Forwarded-For", false},
+ {net::HttpRequestHeaders::kConnection, "close, X-Real-IP", false},
+ {net::HttpRequestHeaders::kConnection, "Authorization, keep-alive",
+ false},
+ {net::HttpRequestHeaders::kConnection, "", true},
+ {net::HttpRequestHeaders::kConnection, " ", true},
+ {net::HttpRequestHeaders::kConnection, ",", true},
+ {net::HttpRequestHeaders::kConnection, ",,,", true},
+ {net::HttpRequestHeaders::kConnection, " , , ", true},
+ {net::HttpRequestHeaders::kConnection, "close, ", true},
+ {net::HttpRequestHeaders::kConnection, ", keep-alive", true},
+ {net::HttpRequestHeaders::kConnection, ", keep-alive, , close, ", true},
{net::HttpRequestHeaders::kTransferEncoding, "Chunked", false},
{net::HttpRequestHeaders::kTransferEncoding, "Chunky", false},
{"cOnNeCtIoN", "uPgRaDe", false},
+ {"cOnNeCtIoN", "kEeP-aLiVe", true},
{net::HttpRequestHeaders::kProxyAuthorization,
"Basic Zm9vOmJhcg==", false},
@@ -78,6 +94,18 @@
{net::HttpRequestHeaders::kConnection, "Upgrade", false},
{net::HttpRequestHeaders::kConnection, "Close", true},
+ {net::HttpRequestHeaders::kConnection, "keep-alive", true},
+ {net::HttpRequestHeaders::kConnection, "keep-alive, Upgrade", false},
+ {net::HttpRequestHeaders::kConnection, "X-Forwarded-For", false},
+ {net::HttpRequestHeaders::kConnection, "close, X-Real-IP", false},
+ {net::HttpRequestHeaders::kConnection, "", true},
+ {net::HttpRequestHeaders::kConnection, " ", true},
+ {net::HttpRequestHeaders::kConnection, ",", true},
+ {net::HttpRequestHeaders::kConnection, ",,,", true},
+ {net::HttpRequestHeaders::kConnection, " , , ", true},
+ {net::HttpRequestHeaders::kConnection, "close, ", true},
+ {net::HttpRequestHeaders::kConnection, ", keep-alive", true},
+ {net::HttpRequestHeaders::kConnection, ", keep-alive, , close, ", true},
{net::HttpRequestHeaders::kTransferEncoding, "Chunked", false},
{net::HttpRequestHeaders::kTransferEncoding, "Chunky", false},
{"cOnNeCtIoN", "uPgRaDe", false},
Original Bug Report
Renderer can control Connection header, enabling hop-by-hop header stripping in HTTP/1.1
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team.
Overview: A compromised renderer can specify arbitrary values in the Connection header because the Network Service’s safety check only blocks the value Upgrade. This allows an attacker to exploit hop-by-hop header stripping on HTTP/1.1 intermediaries, potentially bypassing security controls like IP allowlists on credentialed cross-origin requests.
Affected files:
services/network/public/cpp/header_util.ccnet/http/http_network_transaction.ccnet/http/http_stream_parser.ccservices/network/cors/cors_url_loader_factory.ccservices/network/cors/cors_url_loader.ccservices/network/url_loader.ccservices/network/url_loader_util.ccservices/network/public/cpp/http_request_headers_mojom_traits.cc
Estimated timestamp from git blame: 2019-10-08
Summary
The Network Service uses network::AreRequestHeadersSafe (services/network/public/cpp/header_util.cc) to validate HTTP headers sent by a renderer process. However, the Connection header is not present in the kUnsafeHeaders list. Instead, it is only checked in kUnsafeHeaderValues for the exact, case-insensitive string “Upgrade”.
Because any other value is permitted, a compromised renderer can send a custom Connection header (e.g., Connection: X-Forwarded-For). According to RFC 7230 §6.1, compliant intermediaries must treat any header listed in the Connection header as a hop-by-hop header and strip it before forwarding the request. This allows an attacker to forcefully strip security-critical headers (like X-Forwarded-For, X-Real-IP, or internal authentication tokens) on requests targeting victim sites over HTTP/1.1.
Technical Details
- Validation Gap: In
services/network/public/cpp/header_util.cc,IsRequestHeaderSafeallows theConnectionheader as long as the value is not “Upgrade”. - CORS Preflight Bypass:
Connectionis defined as a forbidden header bynet::HttpUtil::IsSafeHeader. Consequently,CorsUnsafeNotForbiddenRequestHeaderNames(services/network/cors/cors_util.cc) ignores it, meaning the customConnectionheader does not trigger a CORS preflight. - Header Overwriting: In
net::HttpNetworkTransaction::BuildRequestHeaders(net/http/http_network_transaction.cc), a defaultConnection: keep-aliveis set. Later in the same method,request_headers_.MergeFrom(request_->extra_headers)is called. This finds the existingConnectionheader and overwrites it with the attacker’s value. - Verbatim Serialization: For HTTP/1.1,
net::HttpStreamParser::SendRequestserializes the headers verbatim, sending the attacker-controlledConnectionheader over the wire.
Potential Reproduction Steps
Note: These are potential steps based on code analysis; a working proof-of-concept has not been executed.
- Gain arbitrary code execution in a renderer process.
- Obtain a valid
network::mojom::URLLoaderFactory(e.g., the frame’s subresource factory). - Construct a
network::ResourceRequesttargetinghttps://victim.example/(reached over HTTP/1.1). - Set
credentials_modetokIncludeto attach the user’s cookies for the victim site. - Inject the custom header into
request.headers, e.g.,{"Connection": "X-Forwarded-For"}. - Invoke
factory->CreateLoaderAndStart()via Mojo. - The Network Service merges the header, overwriting the default
keep-alive, and sends it to the victim site without a CORS preflight. - An RFC-compliant intermediary in front of the victim site processes the request, sees
Connection: X-Forwarded-For, and strips theX-Forwarded-Forheader before forwarding it to the backend.
Suggested Fix
The Connection header should be completely blocked from being set by untrusted callers, aligning with the Fetch specification’s forbidden header names. Move net::HttpRequestHeaders::kConnection from kUnsafeHeaderValues to the kUnsafeHeaders list in services/network/public/cpp/header_util.cc.
Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.