CVE-2026-7969
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
fornet/base/elements_upload_data_stream.cc |
modified | |
SimpleHugeElementReadernet/base/elements_upload_data_stream_unittest.cc |
modified | |
ElementsUploadDataStreamTestnet/base/elements_upload_data_stream_unittest.cc |
modified | |
TEST_Fnet/base/elements_upload_data_stream_unittest.cc |
modified | |
ifnet/base/upload_data_stream.cc |
modified |
Files Changed
net/base/elements_upload_data_stream.ccnet/base/elements_upload_data_stream_unittest.ccnet/base/upload_data_stream.cc
Patch
From 4c99d24bb6d59e971ca9a1fc2802d2504213f4b5 Mon Sep 17 00:00:00 2001 From: Andrew Paseltiner <[email protected]> Date: Tue, 31 Mar 2026 11:54:24 -0700 Subject: [PATCH] [net] Fix integer overflow in ElementsUploadDataStream::InitElements When summing the sizes of multi-element upload bodies, an integer overflow could occur, leading to an incorrect (smaller) Content-Length header being sent while still transmitting the full request body. This could be exploited for HTTP Request Smuggling. This CL: 1. Uses base::CheckedNumeric to detect overflow in InitElements and returns ERR_FILE_TOO_BIG if it occurs. 2. Hardens UploadDataStream::OnReadCompleted by upgrading a DCHECK_LE to CHECK_LE, ensuring the stream terminates correctly even if a reader overshoots its promised size. 3. Adds a regression test for total size overflow in elements_upload_data_stream_unittest.cc. Fixed: 497450574 Change-Id: Ib4118c152e03546434287cccaf60ad3cc3e3f51d Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7715800 Reviewed-by: mmenke <[email protected]> Commit-Queue: Andrew Paseltiner <[email protected]> Cr-Commit-Position: refs/heads/main@{#1607998} --- diff --git a/net/base/elements_upload_data_stream.cc b/net/base/elements_upload_data_stream.cc index 16589b8e..2bb983d 100644 --- a/net/base/elements_upload_data_stream.cc +++ b/net/base/elements_upload_data_stream.cc @@ -8,6 +8,7 @@ #include "base/check_op.h" #include "base/functional/bind.h" +#include "base/numerics/checked_math.h" #include "net/base/io_buffer.h" #include "net/base/net_errors.h" #include "net/base/upload_bytes_element_reader.h" @@ -76,11 +77,16 @@ return result; } - uint64_t total_size = 0; + base::CheckedNumeric<uint64_t> total_size = 0; for (const std::unique_ptr<UploadElementReader>& it : element_readers_) { total_size += it->GetContentLength(); } - SetSize(total_size); + + if (!total_size.IsValid()) { + return ERR_FILE_TOO_BIG; + } + + SetSize(total_size.ValueOrDie()); return OK; } diff --git a/net/base/elements_upload_data_stream_unittest.cc b/net/base/elements_upload_data_stream_unittest.cc index 4818ebb..a1fa5b4 100644 --- a/net/base/elements_upload_data_stream_unittest.cc +++ b/net/base/elements_upload_data_stream_unittest.cc @@ -137,6 +137,24 @@ int read_result_ = OK; }; +class SimpleHugeElementReader : public UploadElementReader { + public: + explicit SimpleHugeElementReader(uint64_t length) : length_(length) {} + + int Init(CompletionOnceCallback callback) override { return OK; } + uint64_t GetContentLength() const override { return length_; } + uint64_t BytesRemaining() const override { return length_; } + bool IsInMemory() const override { return true; } + int Read(IOBuffer* buf, + int buf_length, + CompletionOnceCallback callback) override { + return 0; + } + + private: + uint64_t length_; +}; + } // namespace class ElementsUploadDataStreamTest : public PlatformTest, @@ -843,4 +861,18 @@ EXPECT_FALSE(read_callback1.have_result()); } +// Regression test for http://crbug.com/497450574. +TEST_F(ElementsUploadDataStreamTest, TotalSizeOverflow) { + element_readers_.push_back(std::make_unique<SimpleHugeElementReader>( + std::numeric_limits<uint64_t>::max())); + element_readers_.push_back(std::make_unique<SimpleHugeElementReader>(10)); + + std::unique_ptr<UploadDataStream> stream( + std::make_unique<ElementsUploadDataStream>(std::move(element_readers_), + 0)); + + ASSERT_THAT(stream->Init(CompletionOnceCallback(), NetLogWithSource()), + IsError(ERR_FILE_TOO_BIG)); +} + } // namespace net diff --git a/net/base/upload_data_stream.cc b/net/base/upload_data_stream.cc index 4481340..9549dfb29 100644 --- a/net/base/upload_data_stream.cc +++ b/net/base/upload_data_stream.cc @@ -173,9 +173,10 @@ if (result > 0) { current_position_ += result; if (!is_chunked_) { - DCHECK_LE(current_position_, total_size_); - if (current_position_ == total_size_) + CHECK_LE(current_position_, total_size_); + if (current_position_ == total_size_) { is_eof_ = true; + } } }
Regression Test / PoC
diff --git a/net/base/elements_upload_data_stream_unittest.cc b/net/base/elements_upload_data_stream_unittest.cc
index 4818ebb..a1fa5b4 100644
--- a/net/base/elements_upload_data_stream_unittest.cc
+++ b/net/base/elements_upload_data_stream_unittest.cc
@@ -137,6 +137,24 @@
int read_result_ = OK;
};
+class SimpleHugeElementReader : public UploadElementReader {
+ public:
+ explicit SimpleHugeElementReader(uint64_t length) : length_(length) {}
+
+ int Init(CompletionOnceCallback callback) override { return OK; }
+ uint64_t GetContentLength() const override { return length_; }
+ uint64_t BytesRemaining() const override { return length_; }
+ bool IsInMemory() const override { return true; }
+ int Read(IOBuffer* buf,
+ int buf_length,
+ CompletionOnceCallback callback) override {
+ return 0;
+ }
+
+ private:
+ uint64_t length_;
+};
+
} // namespace
class ElementsUploadDataStreamTest : public PlatformTest,
@@ -843,4 +861,18 @@
EXPECT_FALSE(read_callback1.have_result());
}
+// Regression test for http://crbug.com/497450574.
+TEST_F(ElementsUploadDataStreamTest, TotalSizeOverflow) {
+ element_readers_.push_back(std::make_unique<SimpleHugeElementReader>(
+ std::numeric_limits<uint64_t>::max()));
+ element_readers_.push_back(std::make_unique<SimpleHugeElementReader>(10));
+
+ std::unique_ptr<UploadDataStream> stream(
+ std::make_unique<ElementsUploadDataStream>(std::move(element_readers_),
+ 0));
+
+ ASSERT_THAT(stream->Init(CompletionOnceCallback(), NetLogWithSource()),
+ IsError(ERR_FILE_TOO_BIG));
+}
+
} // namespace net
Original Bug Report
Integer Overflow in ElementsUploadDataStream Leading to HTTP Request Smuggling
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: An integer overflow exists when summing the sizes of multi-element upload bodies in ElementsUploadDataStream::InitElements. This allows a compromised renderer to provide an extremely large size via Mojo, causing the Content-Length header to wrap to a small value while still transmitting a large, attacker-controlled request body. This discrepancy can be exploited to smuggle HTTP requests and bypass Network Service security checks like CORS and PNA.
Affected files:
net/base/elements_upload_data_stream.ccnet/base/upload_data_stream.ccservices/network/data_pipe_element_reader.ccnet/http/http_stream_parser.ccnet/http/http_network_transaction.ccservices/network/url_loader_util.cc
Estimated timestamp from git blame: 2025-03-13
Description
There is a potential integer overflow vulnerability in net::ElementsUploadDataStream when calculating the total size of an HTTP request body composed of multiple elements (e.g., when a renderer provides multiple data pipes for an upload). This overflow can lead to HTTP/1.1 Request Smuggling.
Detailed Analysis
When a request body contains multiple elements, ElementsUploadDataStream::InitElements (net/base/elements_upload_data_stream.cc) calculates the total size by summing the Content-Length of each element into a uint64_t variable without any overflow checks:
uint64_t total_size = 0;
for (const std::unique_ptr<UploadElementReader>& it : element_readers_) {
total_size += it->GetContentLength();
}
SetSize(total_size);
A compromised renderer can supply multiple kDataPipe elements. The size of each element is determined by the DataPipeGetter::Read Mojo callback. In DataPipeElementReader::ReadCallback (services/network/data_pipe_element_reader.cc), the size provided by the renderer is assigned directly to size_ without validation.
By providing two or more elements with sizes that sum to a value exceeding UINT64_MAX (e.g., UINT64_MAX - 50 and 151), the total_size wraps around to a small value (e.g., 100).
When the request is constructed in net::HttpNetworkTransaction::BuildRequestHeaders, the wrapped total_size (100) is used to emit the Content-Length header. Concurrently, the network service sets Connection: keep-alive for HTTP/1.1 requests.
In UploadDataStream::OnReadCompleted (net/base/upload_data_stream.cc), the logic for determining the end of the stream relies on an exact match between the current_position_ and the total_size_:
if (!is_chunked_) {
DCHECK_LE(current_position_, total_size_);
if (current_position_ == total_size_)
is_eof_ = true;
}
The DCHECK_LE is compiled out in release builds. When HttpStreamParser calls Read on the upload stream, DataPipeElementReader::ReadInternal reads up to the buffer size (16KB) from the Mojo pipe. It does not restrict the read to BytesRemaining().
If the renderer writes more bytes into the pipe than the wrapped total_size_ (e.g., 1100 bytes), current_position_ will overshoot total_size_. Consequently, is_eof_ will never be set to true, and HttpStreamParser will write the full 1100 bytes to the socket.
The server receives the request, reads the first 100 bytes (matching the Content-Length), and treats the remaining 1000 bytes as a subsequent, smuggled HTTP request on the keep-alive connection.
Potential Attacker Steps
(Note: These are suggested steps; we do not currently have a working proof of concept that has been successfully run).
- Compromise a renderer process.
- Create a malicious
network::ResourceRequestBodycontaining multipleDataElementDataPipeelements. - Implement the
network::mojom::DataPipeGetterinterface to return maliciously largesizevalues via theReadcallback, ensuring their sum overflows a 64-bit integer to a small value (e.g., 100). - Initiate a network request (e.g., via
fetch) using the crafted request body. - When the Network Service reads from the data pipes, provide the initial 100 bytes of valid body data, followed immediately by a raw, perfectly formed smuggled HTTP request (e.g.,
GET /admin HTTP/1.1\r\nHost: target.internal\r\n\r\n). - The Network Service will send the undersized
Content-Lengthheader followed by the entire payload, bypassing CORS and PNA checks.
Suggested Fix
- Use
base::CheckedNumericto prevent integer overflows when summing element sizes inElementsUploadDataStream::InitElements. Return an error (e.g.,ERR_FILE_TOO_BIGorERR_INVALID_ARGUMENT) if an overflow occurs. - Harden
UploadDataStream::OnReadCompletedby changing the exact equality check to a greater-than-or-equal check (e.g.,if (current_position_ >= total_size_)), or by explicitly handling the overshoot condition in release builds to prevent indefinite reads iftotal_size_is somehow exceeded.
Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0
Results from 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.