CVE-2026-11016
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifservices/network/data_pipe_element_reader.cc |
modified | |
TEST_Fservices/network/data_pipe_element_reader_unittest.cc |
modified | |
forservices/network/data_pipe_element_reader_unittest.cc |
modified | |
whileservices/network/data_pipe_element_reader_unittest.cc |
modified | |
ifservices/network/data_pipe_element_reader_unittest.cc |
modified |
Files Changed
services/network/data_pipe_element_reader.ccservices/network/data_pipe_element_reader_unittest.cc
Patch
From 9e70dd319cceb3131d44804a26d2c3ff67f989ad Mon Sep 17 00:00:00 2001
From: Matt Menke <mmenke@chromium.org>
Date: Mon, 04 May 2026 09:30:31 -0700
Subject: [PATCH] DataPipeElementReader: Adjust size of data requested over pipe.
The advertised size of the data was not being taken into consideration.
Also check passed in net::Error when receiving the size.
Bug: 497278395
Change-Id: I2b3578689b02ea698b213fa86fd6b9f6bafd8cbd
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7807517
Reviewed-by: Maks Orlovich <morlovich@chromium.org>
Commit-Queue: mmenke <mmenke@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1624748}
---
diff --git a/services/network/data_pipe_element_reader.cc b/services/network/data_pipe_element_reader.cc
index abba3d8..482dac29 100644
--- a/services/network/data_pipe_element_reader.cc
+++ b/services/network/data_pipe_element_reader.cc
@@ -4,6 +4,8 @@
#include "services/network/data_pipe_element_reader.h"
+#include <algorithm>
+
#include "base/check_op.h"
#include "base/containers/span.h"
#include "base/functional/bind.h"
@@ -87,8 +89,12 @@
}
void DataPipeElementReader::ReadCallback(int32_t status, uint64_t size) {
- if (status == net::OK)
+ if (status == net::OK) {
size_ = size;
+ } else if (status > 0 || status == net::ERR_IO_PENDING) {
+ mojo::ReportBadMessage("Only net::Errors allowed.");
+ status = net::ERR_INVALID_ARGUMENT;
+ }
if (init_callback_)
std::move(init_callback_).Run(status);
}
@@ -119,12 +125,15 @@
if (BytesRemaining() == 0)
return net::OK;
- size_t num_bytes = base::checked_cast<size_t>(buf_length);
+ // Don't try to read more bytes than the advertised size of the element.
+ size_t max_bytes = std::min(base::checked_cast<size_t>(buf_length),
+ base::checked_cast<size_t>(BytesRemaining()));
+ size_t bytes_received;
MojoResult rv = data_pipe_->ReadData(MOJO_READ_DATA_FLAG_NONE,
- buf->first(num_bytes), num_bytes);
+ buf->first(max_bytes), bytes_received);
if (rv == MOJO_RESULT_OK) {
- bytes_read_ += num_bytes;
- return base::checked_cast<int>(num_bytes);
+ bytes_read_ += bytes_received;
+ return base::checked_cast<int>(bytes_received);
}
if (rv == MOJO_RESULT_SHOULD_WAIT) {
diff --git a/services/network/data_pipe_element_reader_unittest.cc b/services/network/data_pipe_element_reader_unittest.cc
index 9aacd78..798c004 100644
--- a/services/network/data_pipe_element_reader_unittest.cc
+++ b/services/network/data_pipe_element_reader_unittest.cc
@@ -13,14 +13,18 @@
#include "base/notimplemented.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
+#include "base/strings/string_view_util.h"
#include "base/test/task_environment.h"
+#include "mojo/public/cpp/bindings/message.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/system/data_pipe.h"
#include "mojo/public/cpp/system/data_pipe_utils.h"
#include "net/base/io_buffer.h"
#include "net/base/test_completion_callback.h"
+#include "net/test/gtest_util.h"
#include "services/network/public/mojom/data_pipe_getter.mojom.h"
+#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
// Most tests of this class are at the URLLoader layer. These tests focus on
@@ -99,6 +103,79 @@
DataPipeElementReader element_reader_;
};
+// Test that if a bad status code is passed to the ReadCallback, initialization
+// fails.
+TEST_F(DataPipeElementReaderTest, BadStatusCode) {
+ // Returned as final net error codes, but these are invalid completion codes.
+ const int test_cases[] = {net::ERR_IO_PENDING, 1};
+
+ for (int test_case : test_cases) {
+ SCOPED_TRACE(test_case);
+
+ net::TestCompletionCallback init_callback;
+ ASSERT_THAT(element_reader_.Init(init_callback.callback()),
+ net::test::IsError(net::ERR_IO_PENDING));
+
+ // Wait for DataPipeGetter::Read() to be called.
+ mojo::ScopedDataPipeProducerHandle write_pipe;
+ network::mojom::DataPipeGetter::ReadCallback read_pipe_callback;
+ data_pipe_getter_.WaitForRead(&write_pipe, &read_pipe_callback);
+
+ // Pass in the bad Error code, along with a size that should be ignored.
+ std::move(read_pipe_callback).Run(test_case, 100);
+
+ EXPECT_THAT(init_callback.WaitForResult(),
+ net::test::IsError(net::ERR_INVALID_ARGUMENT));
+ }
+}
+
+// Test the case where a caller tries to write more data than is requested. The
+// extra bytes should be ignored.
+TEST_F(DataPipeElementReaderTest, TooMuchWritten) {
+ // Body that's written over the pipe.
+ std::string body = "body+";
+ // Advertised size of the body.
+ size_t advertised_size = body.size() - 1;
+
+ // The network stack calls Init.
+ net::TestCompletionCallback init_callback;
+ ASSERT_THAT(element_reader_.Init(init_callback.callback()),
+ net::test::IsError(net::ERR_IO_PENDING));
+
+ // Wait for DataPipeGetter::Read() to be called.
+ mojo::ScopedDataPipeProducerHandle write_pipe;
+ network::mojom::DataPipeGetter::ReadCallback read_pipe_callback;
+ data_pipe_getter_.WaitForRead(&write_pipe, &read_pipe_callback);
+ std::move(read_pipe_callback).Run(net::OK, advertised_size);
+
+ ASSERT_THAT(init_callback.WaitForResult(), net::test::IsOk());
+ ASSERT_EQ(element_reader_.GetContentLength(), advertised_size);
+
+ // Write the full body. Even though we aren't reading anything yet, the body
+ // is short enough that it should be buffered.
+ mojo::BlockingCopyFromString(body, write_pipe);
+
+ // Try to read from the body. It should typically be consumed in a single
+ // read, with the next read returning net::OK / 0, but handle multiple reads
+ // as well.
+ std::string read_data;
+ while (true) {
+ EXPECT_EQ(element_reader_.BytesRemaining(),
+ advertised_size - read_data.size());
+ auto io_buffer = base::MakeRefCounted<net::IOBufferWithSize>(10);
+ net::TestCompletionCallback read_callback;
+ int bytes_read = read_callback.GetResult(element_reader_.Read(
+ io_buffer.get(), io_buffer->size(), read_callback.callback()));
+ if (bytes_read == net::OK) {
+ break;
+ }
+ ASSERT_GT(bytes_read, 0);
+ read_data.append(base::as_string_view(io_buffer->first(bytes_read)));
+ }
+ EXPECT_EQ(read_data, body.substr(0, advertised_size));
+ EXPECT_EQ(element_reader_.BytesRemaining(), 0u);
+}
+
// Test the case where a second Init() call occurs when there's a pending Init()
// call in progress. The first call should be dropped, in favor of the second
// one.
Regression Test / PoC
diff --git a/services/network/data_pipe_element_reader_unittest.cc b/services/network/data_pipe_element_reader_unittest.cc
index 9aacd78..798c004 100644
--- a/services/network/data_pipe_element_reader_unittest.cc
+++ b/services/network/data_pipe_element_reader_unittest.cc
@@ -13,14 +13,18 @@
#include "base/notimplemented.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
+#include "base/strings/string_view_util.h"
#include "base/test/task_environment.h"
+#include "mojo/public/cpp/bindings/message.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/system/data_pipe.h"
#include "mojo/public/cpp/system/data_pipe_utils.h"
#include "net/base/io_buffer.h"
#include "net/base/test_completion_callback.h"
+#include "net/test/gtest_util.h"
#include "services/network/public/mojom/data_pipe_getter.mojom.h"
+#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
// Most tests of this class are at the URLLoader layer. These tests focus on
@@ -99,6 +103,79 @@
DataPipeElementReader element_reader_;
};
+// Test that if a bad status code is passed to the ReadCallback, initialization
+// fails.
+TEST_F(DataPipeElementReaderTest, BadStatusCode) {
+ // Returned as final net error codes, but these are invalid completion codes.
+ const int test_cases[] = {net::ERR_IO_PENDING, 1};
+
+ for (int test_case : test_cases) {
+ SCOPED_TRACE(test_case);
+
+ net::TestCompletionCallback init_callback;
+ ASSERT_THAT(element_reader_.Init(init_callback.callback()),
+ net::test::IsError(net::ERR_IO_PENDING));
+
+ // Wait for DataPipeGetter::Read() to be called.
+ mojo::ScopedDataPipeProducerHandle write_pipe;
+ network::mojom::DataPipeGetter::ReadCallback read_pipe_callback;
+ data_pipe_getter_.WaitForRead(&write_pipe, &read_pipe_callback);
+
+ // Pass in the bad Error code, along with a size that should be ignored.
+ std::move(read_pipe_callback).Run(test_case, 100);
+
+ EXPECT_THAT(init_callback.WaitForResult(),
+ net::test::IsError(net::ERR_INVALID_ARGUMENT));
+ }
+}
+
+// Test the case where a caller tries to write more data than is requested. The
+// extra bytes should be ignored.
+TEST_F(DataPipeElementReaderTest, TooMuchWritten) {
+ // Body that's written over the pipe.
+ std::string body = "body+";
+ // Advertised size of the body.
+ size_t advertised_size = body.size() - 1;
+
+ // The network stack calls Init.
+ net::TestCompletionCallback init_callback;
+ ASSERT_THAT(element_reader_.Init(init_callback.callback()),
+ net::test::IsError(net::ERR_IO_PENDING));
+
+ // Wait for DataPipeGetter::Read() to be called.
+ mojo::ScopedDataPipeProducerHandle write_pipe;
+ network::mojom::DataPipeGetter::ReadCallback read_pipe_callback;
+ data_pipe_getter_.WaitForRead(&write_pipe, &read_pipe_callback);
+ std::move(read_pipe_callback).Run(net::OK, advertised_size);
+
+ ASSERT_THAT(init_callback.WaitForResult(), net::test::IsOk());
+ ASSERT_EQ(element_reader_.GetContentLength(), advertised_size);
+
+ // Write the full body. Even though we aren't reading anything yet, the body
+ // is short enough that it should be buffered.
+ mojo::BlockingCopyFromString(body, write_pipe);
+
+ // Try to read from the body. It should typically be consumed in a single
+ // read, with the next read returning net::OK / 0, but handle multiple reads
+ // as well.
+ std::string read_data;
+ while (true) {
+ EXPECT_EQ(element_reader_.BytesRemaining(),
+ advertised_size - read_data.size());
+ auto io_buffer = base::MakeRefCounted<net::IOBufferWithSize>(10);
+ net::TestCompletionCallback read_callback;
+ int bytes_read = read_callback.GetResult(element_reader_.Read(
+ io_buffer.get(), io_buffer->size(), read_callback.callback()));
+ if (bytes_read == net::OK) {
+ break;
+ }
+ ASSERT_GT(bytes_read, 0);
+ read_data.append(base::as_string_view(io_buffer->first(bytes_read)));
+ }
+ EXPECT_EQ(read_data, body.substr(0, advertised_size));
+ EXPECT_EQ(element_reader_.BytesRemaining(), 0u);
+}
+
// Test the case where a second Init() call occurs when there's a pending Init()
// call in progress. The first call should be dropped, in favor of the second
// one.
Original Bug Report
HTTP Request Smuggling via missing read clamp in DataPipeElementReader
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: DataPipeElementReader fails to clamp Mojo pipe reads to the declared content size. A compromised renderer can exploit this by declaring a small request body size but supplying excess data, which the Network Service blindly sends over the socket. On HTTP/1.1 connections, this surplus data is interpreted by the server as a smuggled request, bypassing security checks like CORS and Fetch Metadata.
Affected files:
services/network/data_pipe_element_reader.ccnet/base/upload_data_stream.ccnet/http/http_stream_parser.cc
Estimated timestamp from git blame: 2025-03-13
Description
There is a potential logic error in DataPipeElementReader::ReadInternal within the Network Service. When reading the request body from a Mojo data pipe, the function fails to clamp the number of bytes requested to the expected remaining bytes (BytesRemaining()).
Instead of clamping, it uses the entire length of the buffer provided by the caller (typically 16KB in HttpStreamParser):
// services/network/data_pipe_element_reader.cc
int DataPipeElementReader::ReadInternal(net::IOBuffer* buf, int buf_length) {
// ...
if (BytesRemaining() == 0)
return net::OK;
// Missing clamp to BytesRemaining() here!
size_t num_bytes = base::checked_cast<size_t>(buf_length);
MojoResult rv = data_pipe_->ReadData(MOJO_READ_DATA_FLAG_NONE,
buf->first(num_bytes), num_bytes);
// ...
}
In contrast, other reader implementations like UploadBytesElementReader correctly enforce this boundary (e.g., using std::min(BytesRemaining(), buf_length)). Because UploadDataStream::OnReadCompleted only asserts bounds using DCHECK_LE (which is compiled out in Release builds), the oversized read successfully propagates and is written to the underlying network socket.
Potential Attack Scenario
Note: These are suggested steps based on static analysis; our tooling has not yet executed a live Proof of Concept.
- Renderer Compromise: An attacker compromises a renderer process and initiates an HTTP/1.1 request to a target server.
- Custom DataPipeGetter: The attacker supplies a custom
network::mojom::DataPipeGetterfor the request body via IPC. - Size Manipulation: When the Network Service calls
Init()on the getter, the attacker responds with a tiny size (e.g.,1byte). The browser sets the outgoing HTTP header toContent-Length: 1. - Smuggled Payload: The attacker writes a much larger payload (up to 16KB) into the Mojo pipe, consisting of 1 byte of junk followed by a fully-formed malicious HTTP request.
- Oversized Read:
HttpStreamParserattempts to send the body and provides a 16KB buffer toDataPipeElementReader::ReadInternal. Due to the missing clamp, the entire malicious payload is read from the Mojo pipe. - Transmission: The Network Service writes the full 16KB to the TCP socket.
- Request Smuggling: The target HTTP/1.1 server reads the
Content-Length: 1header, consumes the first byte as the legitimate request body, and natively parses the remaining data as a second, completely separate HTTP request on the same connection.
Impact
This primitive allows a compromised renderer to fully bypass Network Service defense-in-depth mechanisms, such as CORS preflight enforcement and Fetch Metadata (e.g., Sec-Fetch-Site). The attacker can successfully smuggle requests with spoofed internal headers to vulnerable first-party or internal network endpoints.
Suggested Fix
Update DataPipeElementReader::ReadInternal to clamp the read size to the remaining bytes expected:
size_t num_bytes = std::min(base::checked_cast<size_t>(buf_length),
base::checked_cast<size_t>(BytesRemaining()));
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.