Medium chrome Integer Overflow 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInteger overflow in Network
DescriptionInteger overflow in Network
ComponentNetwork
Bug ClassInteger Overflow
Tracker497450574
Fix commit4c99d24bb6d5 (chromium/src) +43/-4
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-05

Changed Functions

FunctionChangeNotes
for
net/base/elements_upload_data_stream.cc
modified
SimpleHugeElementReader
net/base/elements_upload_data_stream_unittest.cc
modified
ElementsUploadDataStreamTest
net/base/elements_upload_data_stream_unittest.cc
modified
TEST_F
net/base/elements_upload_data_stream_unittest.cc
modified
if
net/base/upload_data_stream.cc
modified

Files Changed

  • net/base/elements_upload_data_stream.cc
  • net/base/elements_upload_data_stream_unittest.cc
  • net/base/upload_data_stream.cc
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;
+      }
     }
   }
 
Loading diff…

Regression Test / PoC

shipped with the fix
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
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • net/base/upload_data_stream.cc
  • services/network/data_pipe_element_reader.cc
  • net/http/http_stream_parser.cc
  • net/http/http_network_transaction.cc
  • services/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).

  1. Compromise a renderer process.
  2. Create a malicious network::ResourceRequestBody containing multiple DataElementDataPipe elements.
  3. Implement the network::mojom::DataPipeGetter interface to return maliciously large size values via the Read callback, ensuring their sum overflows a 64-bit integer to a small value (e.g., 100).
  4. Initiate a network request (e.g., via fetch) using the crafted request body.
  5. 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).
  6. The Network Service will send the undersized Content-Length header followed by the entire payload, bypassing CORS and PNA checks.

Suggested Fix

  1. Use base::CheckedNumeric to prevent integer overflows when summing element sizes in ElementsUploadDataStream::InitElements. Return an error (e.g., ERR_FILE_TOO_BIG or ERR_INVALID_ARGUMENT) if an overflow occurs.
  2. Harden UploadDataStream::OnReadCompleted by 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 if total_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.

View on issue tracker