Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper input validation in Network
DescriptionImproper input validation in Network
ComponentNetwork
Bug ClassLogic Error
Tracker513438970
Fix commitb12b81d1c3bf (chromium/src) +38/-16
CISA KEVNot listed
CreditedGoogle
Disclosed2026-09-08

Changed Functions

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

Files Changed

  • services/network/public/cpp/cors/cors.cc
  • services/network/public/cpp/cors/cors_unittest.cc
  • third_party/blink/web_tests/external/wpt/cors/cors-safelisted-request-header.any.js
From b12b81d1c3bfa70d2f73d27d377dbab77952f322 Mon Sep 17 00:00:00 2001
From: Martin Verde <thesalsa@google.com>
Date: Tue, 04 Aug 2026 19:22:15 -0700
Subject: [PATCH] [CORS] Tighten safelisted Range header value parsing

IsCorsSafelistedHeader() delegated the Range value check to the
permissive RFC-7233 parser net::HttpUtil::ParseRangeHeader, which
accepts values that the Fetch "simple range header value" algorithm
rejects: a leading sign on byte positions, "-1" as last-byte-pos,
and a case-insensitive "bytes=" prefix.

Replace the delegation with net::HttpUtil::ParseFetchSingleRange,
which strictly implements the Fetch "simple range header value"
algorithm (allow_whitespace=false). Additionally, reject values
that overflow int64_t by checking for std::numeric_limits<int64_t>::max().

TAG=agy
CONV=6c0675d2-d765-4cce-9da4-2bd9456132ec

Bug: 513438970
Change-Id: Ie81a1b3b639cf9c5ae119be81c8956066e04fe69
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8189598
Reviewed-by: James Scott <jamescscott@google.com>
Commit-Queue: Martin Pan-Verde <thesalsa@google.com>
Reviewed-by: Kenichi Ishibashi <bashi@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1673857}
---

diff --git a/services/network/public/cpp/cors/cors.cc b/services/network/public/cpp/cors/cors.cc
index ec75705..a176c188 100644
--- a/services/network/public/cpp/cors/cors.cc
+++ b/services/network/public/cpp/cors/cors.cc
@@ -6,6 +6,7 @@
 #include "services/network/public/cpp/cors/cors.h"
 
 #include <algorithm>
+#include <limits>
 #include <set>
 #include <string_view>
 #include <vector>
@@ -391,22 +392,24 @@
     return IsCorsSafelistedLowerCaseContentType(
         lower_value, is_ad_auction_trusted_signals_request);
   } else if (lower_name == "range") {
-    // A 'simple' range value is of the following form: 'bytes=\d+-(\d+)?'.
-    // We can use the regular range header parser with the following caveats:
-    // - No space characters or trailing commas
-    // - Only one range is provided
-    // - No suffix (bytes=-x) ranges
-
-    if (std::ranges::any_of(lower_value, [](char c) {
-          return net::HttpUtil::IsLWS(c) || c == ',';
-        })) {
+    // A 'simple' range value is defined in the Fetch specification:
+    // https://fetch.spec.whatwg.org/#simple-range-header-value
+    // It must have an exact case-sensitive "bytes=" prefix. We also impose
+    // extra restrictions:
+    //  - No whitespace (e.g. space or trailing comma).
+    //  - No suffix ranges (e.g. bytes=-500).
+    std::optional<net::HttpByteRange> range =
+        net::HttpUtil::ParseFetchSingleRange(value,
+                                             /*allow_whitespace=*/false);
+    if (!range || range->IsSuffixByteRange() || !range->IsValid()) {
       return false;
     }
-    std::vector<net::HttpByteRange> ranges;
-    if (!net::HttpUtil::ParseRangeHeader(lower_value, &ranges))
+    // `ParseFetchSingleRange()` will saturate values too high to represent as
+    // `int64_t` to `std::numeric_limits<int64_t>::max()`.
+    if (range->first_byte_position() == std::numeric_limits<int64_t>::max() ||
+        range->last_byte_position() == std::numeric_limits<int64_t>::max()) {
       return false;
-    if (ranges.size() != 1 || ranges[0].IsSuffixByteRange())
-      return false;
+    }
     return true;
   } else if (lower_name == "device-memory" || lower_name == "dpr" ||
              lower_name == "downlink") {
diff --git a/services/network/public/cpp/cors/cors_unittest.cc b/services/network/public/cpp/cors/cors_unittest.cc
index 590471ee1..c0750f7 100644
--- a/services/network/public/cpp/cors/cors_unittest.cc
+++ b/services/network/public/cpp/cors/cors_unittest.cc
@@ -569,11 +569,12 @@
   EXPECT_FALSE(IsCorsSafelistedHeader("range", ""));
   EXPECT_FALSE(IsCorsSafelistedHeader("range", "500"));
 
-  // Case
+  // Case of header name is insensitive, but value unit must be exact lowercase.
   EXPECT_TRUE(IsCorsSafelistedHeader("range", "bytes=100-200"));
   EXPECT_TRUE(IsCorsSafelistedHeader("Range", "bytes=100-200"));
   EXPECT_TRUE(IsCorsSafelistedHeader("RANGE", "bytes=100-200"));
-  EXPECT_TRUE(IsCorsSafelistedHeader("range", "BYTES=100-200"));
+  EXPECT_FALSE(IsCorsSafelistedHeader("range", "BYTES=100-200"));
+  EXPECT_FALSE(IsCorsSafelistedHeader("range", "Bytes=100-200"));
 
   // Valid values
   EXPECT_TRUE(IsCorsSafelistedHeader("range", "bytes=100-"));
@@ -585,12 +586,24 @@
   EXPECT_FALSE(IsCorsSafelistedHeader("range", "bytes=100-200,400-"));
   EXPECT_FALSE(IsCorsSafelistedHeader("range", "bytes=-50,100-"));
 
-  // Invalid ranges
+  // Invalid ranges and formatting variations
   EXPECT_FALSE(IsCorsSafelistedHeader("range", "bytes=200-100"));
   EXPECT_FALSE(IsCorsSafelistedHeader("range", "bytes=-200--100"));
   EXPECT_FALSE(IsCorsSafelistedHeader("range", "bytes=-50-50"));
   EXPECT_FALSE(IsCorsSafelistedHeader("range", "bytes=-200"));
   EXPECT_FALSE(IsCorsSafelistedHeader("range", "bytes=100"));
+  EXPECT_FALSE(IsCorsSafelistedHeader("range", "bytes=+0-100"));
+  EXPECT_FALSE(IsCorsSafelistedHeader("range", "bytes=0-+100"));
+  EXPECT_FALSE(IsCorsSafelistedHeader("range", "bytes=+100-200"));
+  EXPECT_FALSE(IsCorsSafelistedHeader("range", "bytes=100-+200"));
+  EXPECT_FALSE(IsCorsSafelistedHeader("range", "bytes=+100-"));
+  EXPECT_FALSE(IsCorsSafelistedHeader("range", "bytes=+5-"));
+  EXPECT_FALSE(IsCorsSafelistedHeader("range", "bytes=100--1"));
+  EXPECT_FALSE(IsCorsSafelistedHeader("range", "bytes=0--1"));
+  EXPECT_FALSE(IsCorsSafelistedHeader(
+      "range", "bytes=999999999999999999999-999999999999999999999"));
+  EXPECT_FALSE(
+      IsCorsSafelistedHeader("range", "bytes=5-999999999999999999999"));
 
   // Invalid charset.
   EXPECT_FALSE(IsCorsSafelistedHeader("range", "bytes = 100-200"));
diff --git a/third_party/blink/web_tests/external/wpt/cors/cors-safelisted-request-header.any.js b/third_party/blink/web_tests/external/wpt/cors/cors-safelisted-request-header.any.js
index a0a0417d..a5cff84 100644
--- a/third_party/blink/web_tests/external/wpt/cors/cors-safelisted-request-header.any.js
+++ b/third_party/blink/web_tests/external/wpt/cors/cors-safelisted-request-header.any.js
@@ -66,6 +66,12 @@
   ["bytes = 100-200", true],
   [",bytes=100-200", true],
   ["bytes=,100-200", true],
+  ["BYTES=100-200", true],
+  ["Bytes=100-200", true],
+  ["bytes=+0-100", true],
+  ["bytes=0-+100", true],
+  ["bytes=+5-", true],
+  ["bytes=0--1", true]
 ].forEach(([value, preflight = false]) => {
   safelist({"range": value}, preflight);
 });
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/services/network/public/cpp/cors/cors_unittest.cc b/services/network/public/cpp/cors/cors_unittest.cc
index 590471ee1..c0750f7 100644
--- a/services/network/public/cpp/cors/cors_unittest.cc
+++ b/services/network/public/cpp/cors/cors_unittest.cc
@@ -569,11 +569,12 @@
   EXPECT_FALSE(IsCorsSafelistedHeader("range", ""));
   EXPECT_FALSE(IsCorsSafelistedHeader("range", "500"));
 
-  // Case
+  // Case of header name is insensitive, but value unit must be exact lowercase.
   EXPECT_TRUE(IsCorsSafelistedHeader("range", "bytes=100-200"));
   EXPECT_TRUE(IsCorsSafelistedHeader("Range", "bytes=100-200"));
   EXPECT_TRUE(IsCorsSafelistedHeader("RANGE", "bytes=100-200"));
-  EXPECT_TRUE(IsCorsSafelistedHeader("range", "BYTES=100-200"));
+  EXPECT_FALSE(IsCorsSafelistedHeader("range", "BYTES=100-200"));
+  EXPECT_FALSE(IsCorsSafelistedHeader("range", "Bytes=100-200"));
 
   // Valid values
   EXPECT_TRUE(IsCorsSafelistedHeader("range", "bytes=100-"));
@@ -585,12 +586,24 @@
   EXPECT_FALSE(IsCorsSafelistedHeader("range", "bytes=100-200,400-"));
   EXPECT_FALSE(IsCorsSafelistedHeader("range", "bytes=-50,100-"));
 
-  // Invalid ranges
+  // Invalid ranges and formatting variations
   EXPECT_FALSE(IsCorsSafelistedHeader("range", "bytes=200-100"));
   EXPECT_FALSE(IsCorsSafelistedHeader("range", "bytes=-200--100"));
   EXPECT_FALSE(IsCorsSafelistedHeader("range", "bytes=-50-50"));
   EXPECT_FALSE(IsCorsSafelistedHeader("range", "bytes=-200"));
   EXPECT_FALSE(IsCorsSafelistedHeader("range", "bytes=100"));
+  EXPECT_FALSE(IsCorsSafelistedHeader("range", "bytes=+0-100"));
+  EXPECT_FALSE(IsCorsSafelistedHeader("range", "bytes=0-+100"));
+  EXPECT_FALSE(IsCorsSafelistedHeader("range", "bytes=+100-200"));
+  EXPECT_FALSE(IsCorsSafelistedHeader("range", "bytes=100-+200"));
+  EXPECT_FALSE(IsCorsSafelistedHeader("range", "bytes=+100-"));
+  EXPECT_FALSE(IsCorsSafelistedHeader("range", "bytes=+5-"));
+  EXPECT_FALSE(IsCorsSafelistedHeader("range", "bytes=100--1"));
+  EXPECT_FALSE(IsCorsSafelistedHeader("range", "bytes=0--1"));
+  EXPECT_FALSE(IsCorsSafelistedHeader(
+      "range", "bytes=999999999999999999999-999999999999999999999"));
+  EXPECT_FALSE(
+      IsCorsSafelistedHeader("range", "bytes=5-999999999999999999999"));
 
   // Invalid charset.
   EXPECT_FALSE(IsCorsSafelistedHeader("range", "bytes = 100-200"));
diff --git a/third_party/blink/web_tests/external/wpt/cors/cors-safelisted-request-header.any.js b/third_party/blink/web_tests/external/wpt/cors/cors-safelisted-request-header.any.js
index a0a0417d..a5cff84 100644
--- a/third_party/blink/web_tests/external/wpt/cors/cors-safelisted-request-header.any.js
+++ b/third_party/blink/web_tests/external/wpt/cors/cors-safelisted-request-header.any.js
@@ -66,6 +66,12 @@
   ["bytes = 100-200", true],
   [",bytes=100-200", true],
   ["bytes=,100-200", true],
+  ["BYTES=100-200", true],
+  ["Bytes=100-200", true],
+  ["bytes=+0-100", true],
+  ["bytes=0-+100", true],
+  ["bytes=+5-", true],
+  ["bytes=0--1", true]
 ].forEach(([value, preflight = false]) => {
   safelist({"range": value}, preflight);
 });
Loading diff…

Original Bug Report

reported by vm...@google.com

CORS preflight bypass via non-spec Range header values

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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: Chrome’s CORS implementation incorrectly safelists ‘Range’ headers that deviate from the Fetch specification’s strict grammar. By using leading signs, case-insensitive units, or specific sentinel values, an attacker can bypass mandatory CORS preflight requests to cross-origin servers.

Affected files:

  • services/network/public/cpp/cors/cors.cc
  • net/http/http_util.cc
  • net/http/http_byte_range.cc

Estimated timestamp from git blame: 2021-10-11

Summary

Chrome’s implementation of the CORS-safelisted Range header check is overly permissive, failing to strictly adhere to the Fetch specification’s definition of a ‘simple range header value’. The specification requires the value to follow the strict format: "bytes=" 1*DIGIT "-" *DIGIT.

Due to the use of a permissive integer parser in the network stack, Chrome accepts values with leading signs (e.g., +0), case-insensitive units (e.g., BYTES=), and internal sentinel values (e.g., -1). This allows cross-origin requests with non-conforming Range headers to bypass the CORS preflight (OPTIONS) mechanism.

Root Cause Analysis

The issue resides in the IsCorsSafelistedHeader function within services/network/public/cpp/cors/cors.cc. This function lowercases the header value and then delegates parsing to net::HttpUtil::ParseRangeHeader in net/http/http_util.cc.

  1. Leading Signs: net::HttpUtil::ParseRangeHeader uses base::StringToInt64 to convert the string offsets into integers. base::StringToInt64 permits a leading + sign (e.g., +0), which is forbidden by the Fetch spec’s 1*DIGIT requirement.
  2. Sentinel Collision: A header like Range: bytes=0--1 results in the second token being parsed as -1. In net/http/http_byte_range.cc, -1 is defined as kPositionNotSpecified. Consequently, HttpByteRange::IsValid() treats this as an open-ended range (bytes=0-), which is a simple range and thus incorrectly safelisted.
  3. Case-Insensitive Unit: The code lowercases the header value before processing, allowing BYTES=0-100 to be safelisted. The Fetch specification requires a byte-exact match for the bytes= prefix in the safelist check.

Potential Impact

An attacker can deliver non-standard Range header values to a cross-origin server without the server’s prior consent via a CORS preflight request. This bypasses a layer of defense-in-depth intended to protect servers from unexpected cross-origin traffic and could be used as a building block for parser-differential attacks.

Potential Reproduction Steps (Suggested)

  1. Host an attacker page on https://attacker.test.
  2. Execute a cross-origin fetch from the page:
    fetch('https://victim.example/resource', {
      headers: { 'Range': 'bytes=+0-100' },
      mode: 'cors'
    });
    
  3. Observe the network traffic to victim.example. A GET request should be sent with the literal Range: bytes=+0-100 header without a preceding OPTIONS preflight request.
  4. Confirm that other major browsers (e.g., Firefox) correctly trigger a preflight for the same request.

Suggested Fix

Modify net::HttpUtil::ParseRangeHeader (or the caller in services/network/public/cpp/cors/cors.cc) to use a stricter integer parser, such as net::ParseInt64 with net::ParseIntFormat::NON_NEGATIVE. Additionally, ensure that the bytes= prefix check is case-sensitive when performing CORS safelist validation.

Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

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