Low CVSS 6.5 chrome Input Validation 📄 Reporter bug report 🔧 Commit mapped

Overview

Low
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Network
DescriptionInsufficient validation of untrusted input in Network
ComponentNetwork
Sub-componentOOR-CORS
Bug ClassInput Validation
Tracker494800494
Fix commit1917c17d0ad2 (chromium/src) +331/-5
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N
CISA KEVNot listed
CreditedTianyi Hu
Reported2026-03-21 · bounty $1000 (per vendor advisory)
Fix landed2026-04-23 (public git)
Disclosed2026-06-02
Patch gap40 days — fix public in source before the advisory

Changed Functions

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

Files Changed

  • services/network/cors/cors_url_loader.cc
  • services/network/cors/cors_url_loader_factory.cc
  • services/network/cors/cors_url_loader_factory.h
  • services/network/public/cpp/features.cc
  • services/network/public/cpp/features.h
  • services/network/public/cpp/header_util.cc
From 1917c17d0ad2fdc3e994a9aefd254b8db7bbe457 Mon Sep 17 00:00:00 2001
From: Takashi Toyoshima <[email protected]>
Date: Wed, 22 Apr 2026 19:42:38 -0700
Subject: [PATCH] OOR-CORS: Enforce extra forbidden Sec- header checks

This CL adds a new function ContainsForbiddenSecurityHeader in
header_util to check for unauthorized Sec- headers from renderer.

This function is used in both CorsURLLoaderFactory::IsValidRequest and
CorsURLLoader::FollowRedirect to prevent renderer from injecting or
modifying these headers, while permitting Client Hints and Sec-Purpose.

Change-Id: Ia2923ccbddddba2657d72916a03e657c1785cd85
Bug: 494800494
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7760510
Reviewed-by: Kenichi Ishibashi <[email protected]>
Reviewed-by: Yoshisato Yanagisawa <[email protected]>
Commit-Queue: Takashi Toyoshima <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1619285}
---

diff --git a/services/network/cors/cors_url_loader.cc b/services/network/cors/cors_url_loader.cc
index 3014a28f..6c236903 100644
--- a/services/network/cors/cors_url_loader.cc
+++ b/services/network/cors/cors_url_loader.cc
@@ -456,8 +456,17 @@
     return;
   }
 
+  net::HttpRequestHeaders mutable_modified_headers = modified_headers;
+  if (!process_id_.is_browser() &&
+      ContainsForbiddenSecurityHeader(mutable_modified_headers)) {
+    mojo::ReportBadMessage(
+        "CorsURLLoader: Forbidden Sec- header from renderer in FollowRedirect");
+    HandleComplete(URLLoaderCompletionStatus(net::ERR_INVALID_ARGUMENT));
+    return;
+  }
+
   // Does not allow modifying headers that are stored in `cors_exempt_headers`.
-  for (const auto& header : modified_headers.GetHeaderVector()) {
+  for (const auto& header : mutable_modified_headers.GetHeaderVector()) {
     if (request_.cors_exempt_headers.HasHeader(header.key)) {
       LOG(WARNING) << "A client is trying to modify header value for '"
                    << header.key << "', but it is not permitted.";
@@ -480,9 +489,10 @@
     request_.headers.RemoveHeader(name);
     request_.cors_exempt_headers.RemoveHeader(name);
   }
-  request_.headers.MergeFrom(modified_headers);
 
-  if (GetSecSharedStorageWritableHeader(modified_headers)) {
+  request_.headers.MergeFrom(mutable_modified_headers);
+
+  if (GetSecSharedStorageWritableHeader(mutable_modified_headers)) {
     request_.shared_storage_writable_eligible = true;
   } else if (std::ranges::contains(removed_headers,
                                    kSecSharedStorageWritableHeader)) {
diff --git a/services/network/cors/cors_url_loader_factory.cc b/services/network/cors/cors_url_loader_factory.cc
index bf02d66..b83f8b4 100644
--- a/services/network/cors/cors_url_loader_factory.cc
+++ b/services/network/cors/cors_url_loader_factory.cc
@@ -583,7 +583,7 @@
 }
 
 bool CorsURLLoaderFactory::IsValidRequest(
-    const ResourceRequest& request,
+    ResourceRequest& request,
     uint32_t options,
     const net::MutableNetworkTrafficAnnotationTag& traffic_annotation) {
   if (request.url.SchemeIs(url::kDataScheme)) {
@@ -863,6 +863,13 @@
     return false;
   }
 
+  if (!process_id_.is_browser() &&
+      ContainsForbiddenSecurityHeader(request.headers)) {
+    mojo::ReportBadMessage(
+        "CorsURLLoaderFactory: Forbidden Sec- header from renderer");
+    return false;
+  }
+
   if (!AreRequestHeadersSafe(request.headers) ||
       !AreRequestHeadersSafe(request.cors_exempt_headers)) {
     return false;
diff --git a/services/network/cors/cors_url_loader_factory.h b/services/network/cors/cors_url_loader_factory.h
index fb44c6f4..565a58aa 100644
--- a/services/network/cors/cors_url_loader_factory.h
+++ b/services/network/cors/cors_url_loader_factory.h
@@ -159,7 +159,7 @@
   void DestroyCorsURLLoader(CorsURLLoader* loader);
 
   bool IsValidRequest(
-      const ResourceRequest& request,
+      ResourceRequest& request,
       uint32_t options,
       const net::MutableNetworkTrafficAnnotationTag& traffic_annotation);
 
diff --git a/services/network/public/cpp/features.cc b/services/network/public/cpp/features.cc
index 97cdcac..c4458bf6 100644
--- a/services/network/public/cpp/features.cc
+++ b/services/network/public/cpp/features.cc
@@ -652,4 +652,7 @@
                    "initial_doh_probe_timeout",
                    base::Seconds(5));
 
+BASE_FEATURE(kRestrictForbiddenSecurityHeaders,
+             base::FEATURE_ENABLED_BY_DEFAULT);
+
 }  // namespace network::features
diff --git a/services/network/public/cpp/features.h b/services/network/public/cpp/features.h
index 55aacccf..3d11d098 100644
--- a/services/network/public/cpp/features.h
+++ b/services/network/public/cpp/features.h
@@ -445,6 +445,9 @@
 COMPONENT_EXPORT(NETWORK_CPP_FLAGS_AND_SWITCHES)
 BASE_DECLARE_FEATURE_PARAM(base::TimeDelta, kDelayInitialDohProbeTimeoutParam);
 
+COMPONENT_EXPORT(NETWORK_CPP_FLAGS_AND_SWITCHES)
+BASE_DECLARE_FEATURE(kRestrictForbiddenSecurityHeaders);
+
 }  // namespace network::features
 
 #endif  // SERVICES_NETWORK_PUBLIC_CPP_FEATURES_H_
diff --git a/services/network/public/cpp/header_util.cc b/services/network/public/cpp/header_util.cc
index 18ced593..ca0abc8 100644
--- a/services/network/public/cpp/header_util.cc
+++ b/services/network/public/cpp/header_util.cc
@@ -4,16 +4,20 @@
 
 #include "services/network/public/cpp/header_util.h"
 
+#include <map>
 #include <string>
 #include <vector>
 
 #include "base/containers/fixed_flat_map.h"
+#include "base/feature_list.h"
+#include "base/metrics/histogram_functions.h"
 #include "base/strings/string_split.h"
 #include "base/strings/string_util.h"
 #include "net/base/mime_sniffer.h"
 #include "net/http/http_request_headers.h"
 #include "net/http/http_response_headers.h"
 #include "net/http/http_status_code.h"
+#include "services/network/public/cpp/features.h"
 #include "services/network/public/mojom/url_response_head.mojom.h"
 #include "url/gurl.h"
 
@@ -105,6 +109,94 @@
   return true;
 }
 
+bool ContainsForbiddenSecurityHeader(net::HttpRequestHeaders& headers) {
+  static const bool enabled =
+      base::FeatureList::IsEnabled(features::kRestrictForbiddenSecurityHeaders);
+  if (!enabled) {
+    return false;
+  }
+
+  std::map<std::string, std::string> headers_to_truncate;
+
+  auto sanitize_and_check_security_header = [&](std::string_view name,
+                                                std::string_view value) {
+    // Client Hints are harmless and set by the renderer.
+    if (base::StartsWith(name, "Sec-CH-",
+                         base::CompareCase::INSENSITIVE_ASCII)) {
+      size_t size = value.size();
+      base::UmaHistogramCounts10000("NetworkService.SecCHHeaderSize", size);
+      if (size > 1024) {
+        headers_to_truncate[std::string(name)] =
+            std::string(value.substr(0, 1024));
+      }
+      return true;
+    }
+    // Sec-Purpose is used for prefetch hints and contains short strings.
+    if (base::EqualsCaseInsensitiveASCII(name, "Sec-Purpose")) {
+      return value.size() < 256;
+    }
+    // Browsing Topics API headers contain structured interest tokens.
+    if (base::EqualsCaseInsensitiveASCII(name, "Sec-Browsing-Topics")) {
+      return value.size() < 1024;
+    }
+    // Shared Storage and FLEDGE fetch headers use structured boolean "?1".
+    if (base::EqualsCaseInsensitiveASCII(name, "Sec-Shared-Storage-Writable") ||
+        base::EqualsCaseInsensitiveASCII(name, "Sec-Ad-Auction-Fetch")) {
+      return value == "?1";
+    }
+    // Shared Storage data origin headers contain origin URLs.
+    if (base::EqualsCaseInsensitiveASCII(name,
+                                         "Sec-Shared-Storage-Data-Origin")) {
+      return value.size() <= 267;
+    }
+    // Speculation Rules headers contain comma-separated tokens.
+    if (base::EqualsCaseInsensitiveASCII(name, "Sec-Speculation-Tags")) {
+      size_t size = value.size();
+      base::UmaHistogramCounts10000(
+          "NetworkService.SecSpeculationTagsHeaderSize", size);
+      if (size > 2048) {
+        std::string_view truncated_value = value.substr(0, 2048);
+        size_t last_comma = truncated_value.rfind(',');
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 d876da32..fc2feddf 100644
--- a/services/network/public/cpp/header_util_unittest.cc
+++ b/services/network/public/cpp/header_util_unittest.cc
@@ -8,6 +8,7 @@
 #include "base/strings/stringprintf.h"
 #include "net/http/http_request_headers.h"
 #include "net/http/http_response_headers.h"
+#include "services/network/public/cpp/features.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
 namespace network {
@@ -143,4 +144,174 @@
   }
 }
 
+TEST(HeaderUtilTest, ContainsForbiddenSecurityHeader) {
+  net::HttpRequestHeaders headers;
+
+  // Normal case
+  headers.SetHeader("Sec-CH-UA", "Normal Value");
+  EXPECT_FALSE(ContainsForbiddenSecurityHeader(headers));
+  std::string value;
+  auto value_opt = headers.GetHeader("Sec-CH-UA");
+  ASSERT_TRUE(value_opt.has_value());
+  value = *value_opt;
+  EXPECT_EQ(value, "Normal Value");
+
+  // Truncation case
+  std::string long_value(2000, 'a');
+  headers.SetHeader("Sec-CH-UA-Long", long_value);
+  EXPECT_FALSE(ContainsForbiddenSecurityHeader(headers));
+  value_opt = headers.GetHeader("Sec-CH-UA-Long");
+  ASSERT_TRUE(value_opt.has_value());
+  value = *value_opt;
+  EXPECT_EQ(value.length(), 1024u);
+  EXPECT_EQ(value, long_value.substr(0, 1024));
+
+  // Boundary cases for Sec-CH-
+  std::string value_1023(1023, 'a');
+  headers.SetHeader("Sec-CH-UA-1023", value_1023);
+  EXPECT_FALSE(ContainsForbiddenSecurityHeader(headers));
+  EXPECT_EQ(*headers.GetHeader("Sec-CH-UA-1023"), value_1023);
+
+  std::string value_1024(1024, 'a');
+  headers.SetHeader("Sec-CH-UA-1024", value_1024);
+  EXPECT_FALSE(ContainsForbiddenSecurityHeader(headers));
+  EXPECT_EQ(*headers.GetHeader("Sec-CH-UA-1024"), value_1024);
+
+  std::string value_1025(1025, 'a');
+  headers.SetHeader("Sec-CH-UA-1025", value_1025);
+  EXPECT_FALSE(ContainsForbiddenSecurityHeader(headers));
+  EXPECT_EQ(headers.GetHeader("Sec-CH-UA-1025")->length(), 1024u);
+
+  // Non-Sec-CH- header should not be truncated even if long
+  headers.SetHeader("X-Custom-Header", long_value);
+  EXPECT_FALSE(ContainsForbiddenSecurityHeader(headers));
+  value_opt = headers.GetHeader("X-Custom-Header");
+  ASSERT_TRUE(value_opt.has_value());
+  value = *value_opt;
+  EXPECT_EQ(value.length(), 2000u);
+
+  // Sec-Shared-Storage-Data-Origin boundary cases
+  net::HttpRequestHeaders origin_headers;
+  std::string origin_267(267, 'a');
+  origin_headers.SetHeader("Sec-Shared-Storage-Data-Origin", origin_267);
+  EXPECT_FALSE(ContainsForbiddenSecurityHeader(origin_headers));
+
+  std::string origin_268(268, 'a');
+  origin_headers.SetHeader("Sec-Shared-Storage-Data-Origin", origin_268);
+  EXPECT_TRUE(ContainsForbiddenSecurityHeader(origin_headers));
+
+  // Forbidden Sec- header
+  headers.SetHeader("Sec-Invalid", "value");
+  EXPECT_TRUE(ContainsForbiddenSecurityHeader(headers));
+}
+
+TEST(HeaderUtilTest,
+     ContainsForbiddenSecurityHeader_SecSpeculationTags_Truncation) {
+  net::HttpRequestHeaders headers;
+
+  // Normal case (< 2048)
+  std::string normal_value = "tag1,tag2";
+  headers.SetHeader("Sec-Speculation-Tags", normal_value);
+  EXPECT_FALSE(ContainsForbiddenSecurityHeader(headers));
+  auto value_opt = headers.GetHeader("Sec-Speculation-Tags");
+  ASSERT_TRUE(value_opt.has_value());
+  EXPECT_EQ(*value_opt, normal_value);
+
+  // Truncation case with comma
+  std::string long_value = std::string(2000, 'a') + "," + std::string(100, 'b');
+  // total size = 2000 + 1 + 100 = 2101 > 2048
+  headers.SetHeader("Sec-Speculation-Tags", long_value);
+  EXPECT_FALSE(ContainsForbiddenSecurityHeader(headers));
+  value_opt = headers.GetHeader("Sec-Speculation-Tags");
+  ASSERT_TRUE(value_opt.has_value());
+  std::string value = *value_opt;
+  EXPECT_LE(value.length(), 2048u);
+  // It should be truncated at the last comma before 2048.
+  // 2000 'a's + 1 comma = 2001 bytes. The next is 'b'.
+  // So it should truncate at the comma.
+  EXPECT_EQ(value, std::string(2000, 'a'));
+
+  // Truncation case without comma (fallback to 2048)
+  std::string very_long_tag(2500, 'c');
+  headers.SetHeader("Sec-Speculation-Tags", very_long_tag);
+  EXPECT_FALSE(ContainsForbiddenSecurityHeader(headers));
+  value_opt = headers.GetHeader("Sec-Speculation-Tags");
+  ASSERT_TRUE(value_opt.has_value());
+  value = *value_opt;
+  EXPECT_EQ(value.length(), 2048u);
+  EXPECT_EQ(value, very_long_tag.substr(0, 2048));
+
+  // Boundary cases for Sec-Speculation-Tags
+  std::string value_2047(2047, 'a');
+  headers.SetHeader("Sec-Speculation-Tags", value_2047);
+  EXPECT_FALSE(ContainsForbiddenSecurityHeader(headers));
+  EXPECT_EQ(*headers.GetHeader("Sec-Speculation-Tags"), value_2047);
+
+  std::string value_2048(2048, 'a');
+  headers.SetHeader("Sec-Speculation-Tags", value_2048);
+  EXPECT_FALSE(ContainsForbiddenSecurityHeader(headers));
+  EXPECT_EQ(*headers.GetHeader("Sec-Speculation-Tags"), value_2048);
+
+  std::string value_2049(2049, 'a');
+  headers.SetHeader("Sec-Speculation-Tags", value_2049);
+  EXPECT_FALSE(ContainsForbiddenSecurityHeader(headers));
+  EXPECT_EQ(headers.GetHeader("Sec-Speculation-Tags")->length(), 2048u);
+
+  // Boundary cases with comma
+  std::string comma_at_2047 = std::string(2047, 'a') + ",b";
+  headers.SetHeader("Sec-Speculation-Tags", comma_at_2047);
+  EXPECT_FALSE(ContainsForbiddenSecurityHeader(headers));
+  EXPECT_EQ(*headers.GetHeader("Sec-Speculation-Tags"), std::string(2047, 'a'));
+
+  std::string comma_at_2048 = std::string(2048, 'a') + ",b";
+  headers.SetHeader("Sec-Speculation-Tags", comma_at_2048);
+  EXPECT_FALSE(ContainsForbiddenSecurityHeader(headers));
+  EXPECT_EQ(*headers.GetHeader("Sec-Speculation-Tags"), std::string(2048, 'a'));
+}
+
+TEST(HeaderUtilTest, ContainsForbiddenSecurityHeader_SecAdAuction_Truncation) {
+  net::HttpRequestHeaders headers;
+
+  // Normal case (< 2048)
+  std::string normal_value = "auction-signal-data";
+  headers.SetHeader("Sec-Ad-Auction-Signals", normal_value);
+  EXPECT_FALSE(ContainsForbiddenSecurityHeader(headers));
+  auto value_opt = headers.GetHeader("Sec-Ad-Auction-Signals");
+  ASSERT_TRUE(value_opt.has_value());
+  EXPECT_EQ(*value_opt, normal_value);
+
+  // Truncation case
+  std::string long_value(2500, 'a');
+  headers.SetHeader("Sec-Ad-Auction-Signals", long_value);
+  EXPECT_FALSE(ContainsForbiddenSecurityHeader(headers));
+  value_opt = headers.GetHeader("Sec-Ad-Auction-Signals");
+  ASSERT_TRUE(value_opt.has_value());
+  std::string value = *value_opt;
+  EXPECT_EQ(value.length(), 2048u);
+  EXPECT_EQ(value, long_value.substr(0, 2048));
+
+  // Boundary cases for Sec-Ad-Auction-Signals
+  std::string value_2047(2047, 'a');
+  headers.SetHeader("Sec-Ad-Auction-Signals", value_2047);
+  EXPECT_FALSE(ContainsForbiddenSecurityHeader(headers));
+  EXPECT_EQ(*headers.GetHeader("Sec-Ad-Auction-Signals"), value_2047);
+
+  std::string value_2048(2048, 'a');
+  headers.SetHeader("Sec-Ad-Auction-Signals", value_2048);
+  EXPECT_FALSE(ContainsForbiddenSecurityHeader(headers));
+  EXPECT_EQ(*headers.GetHeader("Sec-Ad-Auction-Signals"), value_2048);
+
+  std::string value_2049(2049, 'a');
+  headers.SetHeader("Sec-Ad-Auction-Signals", value_2049);
+  EXPECT_FALSE(ContainsForbiddenSecurityHeader(headers));
+  EXPECT_EQ(headers.GetHeader("Sec-Ad-Auction-Signals")->length(), 2048u);
+
+  // Sec-Ad-Auction-Fetch should still be strictly checked
+  headers.SetHeader("Sec-Ad-Auction-Fetch", "?1");
+  EXPECT_FALSE(ContainsForbiddenSecurityHeader(headers));
+
+  headers.SetHeader("Sec-Ad-Auction-Fetch", "?0");
+  EXPECT_TRUE(ContainsForbiddenSecurityHeader(headers));
+}
+
 }  // namespace network
Loading diff…

Original Bug Report

reported by [email protected]

AreRequestHeadersSafe() missing Origin and Sec-* prefix blocking — compromised renderer forges Origin and Sec-Fetch-* headers via modified_headers in FollowRedirect


Report description

AreRequestHeadersSafe() missing Origin and Sec-* prefix blocking — compromised renderer forges Origin and Sec-Fetch-* headers via modified_headers in FollowRedirect


Bug location

Where do you want to report your vulnerability?

Chrome VRP – Report security issues affecting the Chrome browser. See program rules

Which URL (or repository) have you found the vulnerability in?

https://source.chromium.org/chromium/chromium/src/+/main:services/network/public/cpp/header_util.cc


The problem

Please describe the technical details of the vulnerability

IsRequestHeaderSafe() in header_util.cc blocks Host and Proxy-* headers but omits Origin and has no Sec- prefix check. A compromised renderer forges Origin and Sec-Fetch-* headers via modified_headers in FollowRedirect(), overwriting values the network service sets on cross-origin redirects.

Vulnerable file: header_util.cc — IsRequestHeaderSafe()

This violates docs/security/compromised-renderers.md: “Compromised renderers shouldn’t be able to control security sensitive HTTP request headers like Host, Origin, or Sec-Fetch-Site.”

On redirect: redirect_util.cc:87 sets Origin: null, then MergeFrom(modified_headers) at line 93 overwrites it. SetFetchMetadataHeaders writes correct Sec-Fetch-* before modified_headers overwrite them.

Additionally, SetSecFetchModeHeader in sec_header_helpers.cc:173 uses SetHeaderIfMissing, so a renderer-set Sec-Fetch-Mode persists on the initial request without any redirect.

Steps to reproduce:

  1. Check out stable tag: git checkout 146.0.7680.154
  2. git apply poc_patch.diff
  3. autoninja -C out/Default chrome
  4. Start PoC servers: python3 serve.py
  5. Launch patched Chrome:
    • out/Default/Chromium.app/Contents/MacOS/Chromium --user-data-dir=/tmp/hdr1-test http://localhost:8080/
  6. Click “Run Both” — observe forged headers in the server terminal:
    • Origin: http://127.0.0.1:8081 (should be null on cross-origin redirect)
    • Sec-Fetch-Site: same-origin (should be cross-site)

The renderer patch injects forged Origin and Sec-Fetch-* into modified_headers during ResourceLoader::WillFollowRedirect() on cross-origin redirects. No flags needed.

Bisect:

Introducing commit: eb88b44f8e655 — Matt Menke, 2019-06-18, M77. Created kUnsafeHeaders blocklist without Origin or Sec- prefix. Parent commit has no blocklist. Moved to header_util.cc in 633e7e6277b64. Affected: M77 through M146 (current stable).

Fix: Add Origin to kUnsafeHeaders and add Sec- prefix check in IsRequestHeaderSafe(), matching the existing Proxy- prefix check. Also change SetSecFetchModeHeader to overwrite=true. Attached as fix.diff.

Impact analysis

The PoC demonstrates two concrete bypasses:

  1. CSRF bypass: Victim server receives Origin: http://127.0.0.1:8081 (forged to match victim) instead of null on a cross-origin POST redirect (307). Server-side CSRF validation that checks Origin is defeated.
  2. Fetch Metadata bypass: Victim server receives Sec-Fetch-Site: same-origin instead of cross-site. Server-side resource isolation policies recommended by compromised-renderers.md (line 96) are defeated.

Variant analysis also found Access-Control-Request-Private-Network and Cookie (when 3P-blocked) are forgeable through the same gap.


The cause

What version of Chrome have you found the security issue in?

146.0.7680.154 (Stable)

No, it is not related to a crash.

Choose the type of vulnerability

Other

How would you like to be publicly acknowledged for your report?

Tianyi Hu

View on issue tracker