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-componentCORS
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.
Tracker513438970
Fix commitb12b81d1c3bf (chromium/src) +38/-16
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-05-15 (per vendor advisory)
Fix landed2026-08-05 (public git)
Disclosed2026-09-08
Patch gap34 days — fix public in source before the advisory

Background

CORS-safelisted request header
a request header (like Accept, Content-Type, or Range) whose name/value pair is considered “simple” enough that a cross-origin fetch may send it WITHOUT first issuing a CORS preflight (OPTIONS) request.
`Range` "simple range header value"
the Fetch spec’s strict grammar for a safelistable Range value: a case-sensitive bytes= prefix followed by a single first-last (or first-) range with no sign, no suffix (-N) form, and no whitespace or commas.
`net::HttpUtil::ParseRangeHeader` vs `net::HttpUtil::ParseFetchSingleRange`
the former is the permissive RFC-7233 parser (accepts signs, suffix ranges, and, on lowercased input, a case-insensitive unit), while the latter strictly implements the Fetch “simple range header value” algorithm.
`net::HttpByteRange`
the parsed range object exposing first_byte_position(), last_byte_position(), IsSuffixByteRange(), and IsValid().

Root Cause Analysis

The range branch of IsCorsSafelistedHeader (in services/network/public/cpp/cors/cors.cc) decided whether a Range value was safelisted by lowercasing the value and delegating to net::HttpUtil::ParseRangeHeader, the general-purpose RFC-7233 parser. Because the value was first lowercased and the parser is permissive, several values that the Fetch “simple range header value” algorithm rejects were treated as safelisted: a case-insensitive prefix (BYTES=/Bytes=), a leading + sign on byte positions (bytes=+0-100, bytes=0-+100), -1 in the last-byte-position slot (bytes=0--1), and enormous numeric positions that overflow int64_t. The invariant violated is that a header is only allowed to skip CORS preflight when it exactly matches the spec’s simple-value grammar; the permissive parser let non-conforming values slip past.

The fix replaces the delegation with net::HttpUtil::ParseFetchSingleRange(value, /*allow_whitespace=*/false) operating on the original (case-sensitive) value, then rejects the result if it is a suffix range, invalid, or saturated to std::numeric_limits<int64_t>::max() (the overflow sentinel), so only strictly conforming values are safelisted.

Key insight
The core mistake was reusing the lenient RFC-7233 Range parser (on lowercased input) as a stand-in for the Fetch spec’s stricter “simple range header value” grammar, which over-approximated the safelist and let malformed/oversized Range values bypass CORS preflight; the fix swaps in the spec-faithful ParseFetchSingleRange and adds an explicit overflow-sentinel check.

Attack Path

  1. Craft a non-conforming Range value An attacker’s page issues a cross-origin fetch carrying a Range header whose value is not spec-simple, e.g. BYTES=100-200, bytes=+0-100, bytes=0--1, or an int64-overflowing bytes=5-999999999999999999999.
  2. Header wrongly safelisted Pre-patch, IsCorsSafelistedHeader lowercases the value and accepts it via net::HttpUtil::ParseRangeHeader, classifying the header as CORS-safelisted.
  3. Preflight skipped Because the header is deemed safelisted, the network stack sends the actual cross-origin request without a CORS OPTIONS preflight, so the target server never gets the chance to authorize the unusual Range header.
  4. Server receives unexpected Range The request reaches the cross-origin server with an attacker-chosen, non-standard Range value that should have required explicit opt-in; whether this yields further impact depends on that server’s handling (inferred).

Impact Assessment

An attacker-controlled web page gains the ability to send cross-origin requests with malformed or oversized Range header values while evading the CORS preflight that would normally gate such headers, weakening the CORS safelist invariant in the network service. The precondition is only that the victim visits or is served attacker-controlled script that issues the fetch; the concrete downstream consequence depends on the target server’s behavior and is not demonstrated by the patch (inferred). The advisory classifies this as improper input validation in the Network component and does not report exploitation in the wild.

Changed Functions

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

Files Changed

  • third_party/blink/web_tests/external/wpt/cors/cors-safelisted-request-header.any.js
  • services/network/public/cpp/cors/cors_unittest.cc
  • services/network/public/cpp/cors/cors.cc

Audit Directions

  • Permissive parser reused for a stricter spec
    Flag any place that answers a Fetch/CORS “is this simple/safelisted?” question by reusing a general-purpose network parser (RFC-7233 ranges, RFC content-type, etc.); the general parser’s tolerance becomes a safelist over-approximation.
  • Case-folding before a case-sensitive check
    Audit code that lowercases (or otherwise normalizes) a value before matching a grammar that the spec defines as case-sensitive, since normalization silently accepts variants the spec rejects (e.g. BYTES=).
  • Missing overflow/sentinel handling on parsed integers
    Look for parsed numeric fields consumed without checking the parser’s saturation/overflow sentinel (std::numeric_limits<int64_t>::max()), where oversized inputs pass validation that assumed bounded values.
From b12b81d1c3bfa70d2f73d27d377dbab77952f322 Mon Sep 17 00:00:00 2001
From: Martin Verde <[email protected]>
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 <[email protected]>
Commit-Queue: Martin Pan-Verde <[email protected]>
Reviewed-by: Kenichi Ishibashi <[email protected]>
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 [email protected]

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