Medium watch CVSS 4.3 chrome Input Validation 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper input validation in Network
DescriptionImproper input validation in Network
ComponentNetwork
Sub-componentNetwork
Bug ClassInput Validation
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker501799770
Fix commit4deb569d704e (chromium/src) +44/-18
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-04-12 (per vendor advisory)
Fix landed2026-07-06 (public git)
Disclosed2026-08-25
Patch gap50 days — fix public in source before the advisory

Background

`IsRequestHeaderSafe()`
a helper in services/network/public/cpp/header_util.cc that 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 //net stack via MergeFrom().
`Connection` header
a comma-separated token list (RFC 9110 §7.6.1) in which, besides connection options like close and keep-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 was Connection: Upgrade.
`net::HttpUtil::ValuesIterator`
a //net utility 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.

Key insight
The validator compared a list-valued hop-by-hop header against one bad string, when it should have checked each token in the list. The fix parses the value as a token list and allows only close and keep-alive.

Attack Path

  1. 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 set Connection because 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).
  2. Supply a crafted `Connection` value The client sets Connection to a token list naming other headers (for example close, X-Real-IP or Authorization, keep-alive), or hides an Upgrade token inside a list (keep-alive, Upgrade). The old exact-match check accepted all of these.
  3. Header passes validation and is merged IsRequestHeaderSafe() returns true, so the value reaches the //net request headers via MergeFrom() and goes out on the wire.
  4. 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

An attacker who controls request headers that pass this validator can make on-path HTTP intermediaries drop headers named in 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

FunctionChangeNotes
for
services/network/public/cpp/header_util.cc
modified

Files Changed

  • services/network/public/cpp/header_util_unittest.cc
  • services/network/public/cpp/header_util.cc

Audit Directions

  • List-valued headers checked with whole-string equality
    Look in other header allow/deny lists (CORS safelisting, extension header modification, URLLoader validation) 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 lists
    The remaining TODO still lists Cookie, Date, Expect, Referer and Via. Check whether any of them (for example Expect: 100-continue) lets a less-trusted caller change connection or proxy behaviour.
  • Divergence between validator parsing and `//net` parsing
    Make sure validators tokenize the same way //net and intermediaries do (whitespace, empty elements, case, duplicate headers merged by MergeFrom()), so a value that looks harmless to the check is not read differently downstream.
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},
Loading diff…

Regression Test / PoC

shipped with the fix
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},
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • net/http/http_network_transaction.cc
  • net/http/http_stream_parser.cc
  • services/network/cors/cors_url_loader_factory.cc
  • services/network/cors/cors_url_loader.cc
  • services/network/url_loader.cc
  • services/network/url_loader_util.cc
  • services/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

  1. Validation Gap: In services/network/public/cpp/header_util.cc, IsRequestHeaderSafe allows the Connection header as long as the value is not “Upgrade”.
  2. CORS Preflight Bypass: Connection is defined as a forbidden header by net::HttpUtil::IsSafeHeader. Consequently, CorsUnsafeNotForbiddenRequestHeaderNames (services/network/cors/cors_util.cc) ignores it, meaning the custom Connection header does not trigger a CORS preflight.
  3. Header Overwriting: In net::HttpNetworkTransaction::BuildRequestHeaders (net/http/http_network_transaction.cc), a default Connection: keep-alive is set. Later in the same method, request_headers_.MergeFrom(request_->extra_headers) is called. This finds the existing Connection header and overwrites it with the attacker’s value.
  4. Verbatim Serialization: For HTTP/1.1, net::HttpStreamParser::SendRequest serializes the headers verbatim, sending the attacker-controlled Connection header over the wire.

Potential Reproduction Steps

Note: These are potential steps based on code analysis; a working proof-of-concept has not been executed.

  1. Gain arbitrary code execution in a renderer process.
  2. Obtain a valid network::mojom::URLLoaderFactory (e.g., the frame’s subresource factory).
  3. Construct a network::ResourceRequest targeting https://victim.example/ (reached over HTTP/1.1).
  4. Set credentials_mode to kInclude to attach the user’s cookies for the victim site.
  5. Inject the custom header into request.headers, e.g., {"Connection": "X-Forwarded-For"}.
  6. Invoke factory->CreateLoaderAndStart() via Mojo.
  7. The Network Service merges the header, overwriting the default keep-alive, and sends it to the victim site without a CORS preflight.
  8. An RFC-compliant intermediary in front of the victim site processes the request, sees Connection: X-Forwarded-For, and strips the X-Forwarded-For header 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.

View on issue tracker