CVE-2026-87573
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifservices/network/public/cpp/cors/cors.cc |
modified |
Files Changed
services/network/public/cpp/cors/cors.ccservices/network/public/cpp/cors/cors_unittest.ccthird_party/blink/web_tests/external/wpt/cors/cors-safelisted-request-header.any.js
Patch
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); });
Regression Test / PoC
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);
});
Original Bug Report
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.ccnet/http/http_util.ccnet/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.
- Leading Signs:
net::HttpUtil::ParseRangeHeaderusesbase::StringToInt64to convert the string offsets into integers.base::StringToInt64permits a leading+sign (e.g.,+0), which is forbidden by the Fetch spec’s1*DIGITrequirement. - Sentinel Collision: A header like
Range: bytes=0--1results in the second token being parsed as-1. Innet/http/http_byte_range.cc,-1is defined askPositionNotSpecified. Consequently,HttpByteRange::IsValid()treats this as an open-ended range (bytes=0-), which is a simple range and thus incorrectly safelisted. - Case-Insensitive Unit: The code lowercases the header value before processing, allowing
BYTES=0-100to be safelisted. The Fetch specification requires a byte-exact match for thebytes=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)
- Host an attacker page on
https://attacker.test. - Execute a cross-origin fetch from the page:
fetch('https://victim.example/resource', { headers: { 'Range': 'bytes=+0-100' }, mode: 'cors' }); - Observe the network traffic to
victim.example. AGETrequest should be sent with the literalRange: bytes=+0-100header without a precedingOPTIONSpreflight request. - 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.