CVE-2026-76033
Overview
Background
- `Content-Encoding` decoding
FilterSourceStream::GetContentEncodingTypesparses the response’sContent-Encodingheader into a list ofSourceStreamTypevalues (kBrotli,kDeflate,kGzip,kZstd), which the network stack uses to build a decoding stream so that consumers see the decompressed body.- `accepted_stream_types`
- an optional per-request set of
SourceStreamTypevalues on theURLRequest(request_->accepted_stream_types()), driven by the DevToolsNetwork.setAcceptedEncodingscommand according to the commit message, which limits which encodings the client claims to support. - Opaque Response Blocking (ORB)
- a cross-origin protection that inspects (sniffs) the body of
no-corscross-origin responses and blocks those that look like HTML, JSON or XML before they reach the renderer; the commit message names it as the downstream feature affected. - "Decoder emulation"
- the removed behaviour in which an encoding outside
accepted_stream_typeswas treated like an unknown encoding, so the body was passed on still compressed instead of being decoded.
Root Cause Analysis
Before the patch, FilterSourceStream::GetContentEncodingTypes took an extra accepted_stream_types argument. If the response’s Content-Encoding listed a kDeflate, kGzip or kZstd type that was not in that set, the function returned an empty vector, and the removed comment says such an encoding is treated “in the same way as SourceStreamType::kUnknown”. The source-stream setup code in net/url_request/url_request_http_job.cc passed request_->accepted_stream_types() into this call, so when the set was restricted (according to the commit message, via DevTools Network.setAcceptedEncodings), the network stack built no decoder and sent the raw compressed bytes downstream. Those downstream consumers assume the body has already been decoded. The commit message says this emulation “has unintended consequences for downstream features such as ORB”. ORB’s content sniffing then looks at compressed bytes rather than the real HTML, JSON or XML payload, which can keep a cross-origin response from being classified (and blocked) correctly (inferred, based on how ORB sniffs and on the advisory’s CORS/cross-origin classification).
The fix removes the accepted_stream_types parameter and the early-return branch. Every supported encoding is now always decoded, and Network.setAcceptedEncodings still controls the Accept-Encoding request header.
Content-Encoding is always decoded, whatever the emulation setting.Attack Path
- Restricted accepted encodings
A DevTools client calls
Network.setAcceptedEncodings, which setsaccepted_stream_typeson outgoing requests (per the commit message). This needs a DevTools/CDP session, such as open DevTools, an automation client, or an extension using the debugger API. Ordinary web content cannot do this alone. - Cross-origin compressed response
A page in that session makes a
no-corscross-origin request (for example, through a subresource element), and the server answers withContent-Encoding: gzip/deflate/zstdthat is not in the accepted set (inferred). - Decoding skipped
Pre-patch
FilterSourceStream::GetContentEncodingTypesreturns an empty vector, so the source-stream setup inurl_request_http_job.ccbuilds no decoder and the compressed body flows on unchanged. - ORB misclassification ORB sniffs the compressed bytes and does not recognise them as HTML, JSON or XML, so it may let a response through that it would have blocked once decoded. Cross-origin bytes then reach the renderer process (inferred).
Impact Assessment
Network.setAcceptedEncodings DevTools emulation to be active, and the server must use an encoding outside the accepted set. The advisory describes the issue only as “Inappropriate implementation in CORS”. The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 4.2.Changed Functions
| Function | Change | Notes |
|---|---|---|
TESTnet/filter/filter_source_stream_unittest.cc |
modified | |
fornet/filter/filter_source_stream_unittest.cc |
modified |
Files Changed
net/url_request/url_request_http_job.ccnet/filter/filter_source_stream.hnet/filter/filter_source_stream_unittest.ccnet/filter/filter_source_stream.cc
Audit Directions
- Emulation and debug knobs that change data shapeCheck other DevTools/CDP emulation overrides (network conditions, user-agent and header overrides, cache bypass) for cases where they change response bytes or metadata that later security checks (ORB, CORB, CORS, MIME sniffing, CSP) depend on, not just request headers.
- Security sniffing on undecoded or transformed bodiesMake sure every path that feeds ORB or MIME sniffing gets the fully decoded body. Look for paths where decoding can be skipped (unknown encodings,
kUnknownfallbacks, nested-depth limits such askMaxNestedSourceStreamDepth) and check whether a skipped decode fails closed rather than open. - Client-controlled sets in the network layerAudit other optional per-request sets or flags on
URLRequest(for example, accepted types and load flags) that silently change processing semantics, to confirm they cannot be set by a less-trusted party or used to weaken cross-origin protections.
Patch
From 8781bebfa7f1f687e854496d14fe071c587a54c1 Mon Sep 17 00:00:00 2001 From: Alex Rudenko <[email protected]> Date: Mon, 03 Aug 2026 23:09:59 -0700 Subject: [PATCH] Stop emulating missing decoders Emulation of missing content decoders has unintended consequences for downstream features such as ORB. This CL removes the emulation of the missing decoders but keeps the Network.setAcceptedEncodings to affect the Accept-Encoding header. Fixed: 516715010 Change-Id: If36f580d7fcc2e856776eba047077965935cacba Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8179012 Reviewed-by: Andrey Kosyakov <[email protected]> Reviewed-by: Tsuyoshi Horo <[email protected]> Commit-Queue: Alex Rudenko <[email protected]> Cr-Commit-Position: refs/heads/main@{#1673121} --- diff --git a/net/filter/filter_source_stream.cc b/net/filter/filter_source_stream.cc index 65ad6e43..7dbf8d91 100644 --- a/net/filter/filter_source_stream.cc +++ b/net/filter/filter_source_stream.cc @@ -107,10 +107,7 @@ } // static -std::vector<SourceStreamType> FilterSourceStream::GetContentEncodingTypes( - const std::optional<base::flat_set<SourceStreamType>>& - accepted_stream_types, - const HttpResponseHeaders& headers) { +std::vector<SourceStreamType> FilterSourceStream::GetContentEncodingTypes(const HttpResponseHeaders& headers) { std::vector<SourceStreamType> types; size_t iter = 0; while (std::optional<std::string_view> type = @@ -121,12 +118,6 @@ case SourceStreamType::kDeflate: case SourceStreamType::kGzip: case SourceStreamType::kZstd: - if (accepted_stream_types && - !accepted_stream_types->contains(source_type)) { - // If the source type is disabled, we treat it - // in the same way as SourceStreamType::kUnknown. - return std::vector<SourceStreamType>(); - } types.push_back(source_type); break; case SourceStreamType::kNone: diff --git a/net/filter/filter_source_stream.h b/net/filter/filter_source_stream.h index be9e706a..deb24279 100644 --- a/net/filter/filter_source_stream.h +++ b/net/filter/filter_source_stream.h @@ -58,11 +58,7 @@ // - No "Content-Encoding" HTTP header is set. // - The value of "Content-Encoding" HTTP header is empty string. // - An unknown encoding type is encountered. - // - An encoding type is found that is not within the accepted_stream_types - // set (if provided). static std::vector<SourceStreamType> GetContentEncodingTypes( - const std::optional<base::flat_set<SourceStreamType>>& - accepted_stream_types, const HttpResponseHeaders& headers); // Creates a chained decoding SourceStream by wrapping the provided `upstream` diff --git a/net/url_request/url_request_http_job.cc b/net/url_request/url_request_http_job.cc index aa7648b..6aa6a7f9f 100644 --- a/net/url_request/url_request_http_job.cc +++ b/net/url_request/url_request_http_job.cc @@ -1538,8 +1538,7 @@ HttpResponseHeaders* headers = GetResponseHeaders(); std::vector<SourceStreamType> types = - FilterSourceStream::GetContentEncodingTypes( - request_->accepted_stream_types(), *headers); + FilterSourceStream::GetContentEncodingTypes(*headers); if (types.size() > kMaxNestedSourceStreamDepth) { return nullptr; diff --git a/net/filter/filter_source_stream_unittest.cc b/net/filter/filter_source_stream_unittest.cc index 900c5f2a..4445178 100644 --- a/net/filter/filter_source_stream_unittest.cc +++ b/net/filter/filter_source_stream_unittest.cc @@ -584,85 +584,53 @@ TEST(FilterSourceStreamTest, GetContentEncodingTypes) { struct { const std::string_view headers; - const std::optional<base::flat_set<SourceStreamType>> accepted_stream_types; const std::vector<SourceStreamType> expected_result; const std::string_view test_comment; } kTestCases[] = { - {"HTTP/1.1 200 OK\n", std::nullopt, {}, "No Content-Encoding header"}, + {"HTTP/1.1 200 OK\n", {}, "No Content-Encoding header"}, {"HTTP/1.1 200 OK\nContent-Encoding: deflate\n", - std::nullopt, {SourceStreamType::kDeflate}, "Single deflate encoding"}, {"HTTP/1.1 200 OK\nContent-Encoding: gzip\n", - std::nullopt, {SourceStreamType::kGzip}, "Single gzip encoding"}, {"HTTP/1.1 200 OK\nContent-Encoding: x-gzip\n", - std::nullopt, {SourceStreamType::kGzip}, "Single x-gzip encoding"}, {"HTTP/1.1 200 OK\nContent-Encoding: br\n", - std::nullopt, {SourceStreamType::kBrotli}, "Single br encoding"}, {"HTTP/1.1 200 OK\nContent-Encoding: zstd\n", - std::nullopt, {SourceStreamType::kZstd}, "Single zstd encoding"}, {"HTTP/1.1 200 OK\nContent-Encoding: br, gzip\n", - std::nullopt, {SourceStreamType::kBrotli, SourceStreamType::kGzip}, "Multiple encodings (brotli and gzip)"}, {"HTTP/1.1 200 OK\nContent-Encoding: gzip, br\n", - std::nullopt, {SourceStreamType::kGzip, SourceStreamType::kBrotli}, "Multiple encodings (gzip and brotli) - different order"}, {"HTTP/1.1 200 OK\nContent-Encoding: unknown\n", - std::nullopt, {}, "Unknown encoding"}, {"HTTP/1.1 200 OK\nContent-Encoding: identity\n", - std::nullopt, {}, "Identity encoding"}, {"HTTP/1.1 200 OK\nContent-Encoding: gzip, unknown\n", - std::nullopt, {}, "Unknown encoding after gzip"}, {"HTTP/1.1 200 OK\nContent-Encoding: gzip, br\n", - std::nullopt, {SourceStreamType::kGzip, SourceStreamType::kBrotli}, "Extra spaces between encodings"}, - {"HTTP/1.1 200 OK\nContent-Encoding: br\n", - base::flat_set<SourceStreamType>({SourceStreamType::kBrotli}), - {SourceStreamType::kBrotli}, - "Accepted types match"}, - {"HTTP/1.1 200 OK\nContent-Encoding: gzip\n", - base::flat_set<SourceStreamType>({SourceStreamType::kBrotli}), - {}, - "No accepted types match"}, - {"HTTP/1.1 200 OK\nContent-Encoding: br, gzip\n", - base::flat_set<SourceStreamType>({SourceStreamType::kBrotli}), - {}, - "Unaccepted type found"}, {"HTTP/1.1 200 OK\ncontent-encoding: GZip\n", - std::nullopt, {SourceStreamType::kGzip}, "Case-insensitive gzip"}, {"HTTP/1.1 200 OK\nContent-Encoding: BR\n", - std::nullopt, {SourceStreamType::kBrotli}, "Case-insensitive brotli"}, {"HTTP/1.1 200 OK\nContent-Encoding: deflate, gzip, br\n", - std::nullopt, {SourceStreamType::kDeflate, SourceStreamType::kGzip, SourceStreamType::kBrotli}, "Three encodings"}, - {"HTTP/1.1 200 OK\nContent-Encoding: deflate, gzip, br\n", - base::flat_set<SourceStreamType>( - {SourceStreamType::kDeflate, SourceStreamType::kBrotli}), - {}, - "Three encodings, two accepted"}, }; for (const auto& test_case : kTestCases) { @@ -670,8 +638,7 @@ auto headers = base::MakeRefCounted<HttpResponseHeaders>( net::HttpUtil::AssembleRawHeaders(test_case.headers)); std::vector<SourceStreamType> types = - FilterSourceStream::GetContentEncodingTypes( - test_case.accepted_stream_types, *headers); + FilterSourceStream::GetContentEncodingTypes(*headers); EXPECT_THAT(types, test_case.expected_result); } }
Regression Test / PoC
diff --git a/net/filter/filter_source_stream_unittest.cc b/net/filter/filter_source_stream_unittest.cc
index 900c5f2a..4445178 100644
--- a/net/filter/filter_source_stream_unittest.cc
+++ b/net/filter/filter_source_stream_unittest.cc
@@ -584,85 +584,53 @@
TEST(FilterSourceStreamTest, GetContentEncodingTypes) {
struct {
const std::string_view headers;
- const std::optional<base::flat_set<SourceStreamType>> accepted_stream_types;
const std::vector<SourceStreamType> expected_result;
const std::string_view test_comment;
} kTestCases[] = {
- {"HTTP/1.1 200 OK\n", std::nullopt, {}, "No Content-Encoding header"},
+ {"HTTP/1.1 200 OK\n", {}, "No Content-Encoding header"},
{"HTTP/1.1 200 OK\nContent-Encoding: deflate\n",
- std::nullopt,
{SourceStreamType::kDeflate},
"Single deflate encoding"},
{"HTTP/1.1 200 OK\nContent-Encoding: gzip\n",
- std::nullopt,
{SourceStreamType::kGzip},
"Single gzip encoding"},
{"HTTP/1.1 200 OK\nContent-Encoding: x-gzip\n",
- std::nullopt,
{SourceStreamType::kGzip},
"Single x-gzip encoding"},
{"HTTP/1.1 200 OK\nContent-Encoding: br\n",
- std::nullopt,
{SourceStreamType::kBrotli},
"Single br encoding"},
{"HTTP/1.1 200 OK\nContent-Encoding: zstd\n",
- std::nullopt,
{SourceStreamType::kZstd},
"Single zstd encoding"},
{"HTTP/1.1 200 OK\nContent-Encoding: br, gzip\n",
- std::nullopt,
{SourceStreamType::kBrotli, SourceStreamType::kGzip},
"Multiple encodings (brotli and gzip)"},
{"HTTP/1.1 200 OK\nContent-Encoding: gzip, br\n",
- std::nullopt,
{SourceStreamType::kGzip, SourceStreamType::kBrotli},
"Multiple encodings (gzip and brotli) - different order"},
{"HTTP/1.1 200 OK\nContent-Encoding: unknown\n",
- std::nullopt,
{},
"Unknown encoding"},
{"HTTP/1.1 200 OK\nContent-Encoding: identity\n",
- std::nullopt,
{},
"Identity encoding"},
{"HTTP/1.1 200 OK\nContent-Encoding: gzip, unknown\n",
- std::nullopt,
{},
"Unknown encoding after gzip"},
{"HTTP/1.1 200 OK\nContent-Encoding: gzip, br\n",
- std::nullopt,
{SourceStreamType::kGzip, SourceStreamType::kBrotli},
"Extra spaces between encodings"},
- {"HTTP/1.1 200 OK\nContent-Encoding: br\n",
- base::flat_set<SourceStreamType>({SourceStreamType::kBrotli}),
- {SourceStreamType::kBrotli},
- "Accepted types match"},
- {"HTTP/1.1 200 OK\nContent-Encoding: gzip\n",
- base::flat_set<SourceStreamType>({SourceStreamType::kBrotli}),
- {},
- "No accepted types match"},
- {"HTTP/1.1 200 OK\nContent-Encoding: br, gzip\n",
- base::flat_set<SourceStreamType>({SourceStreamType::kBrotli}),
- {},
- "Unaccepted type found"},
{"HTTP/1.1 200 OK\ncontent-encoding: GZip\n",
- std::nullopt,
{SourceStreamType::kGzip},
"Case-insensitive gzip"},
{"HTTP/1.1 200 OK\nContent-Encoding: BR\n",
- std::nullopt,
{SourceStreamType::kBrotli},
"Case-insensitive brotli"},
{"HTTP/1.1 200 OK\nContent-Encoding: deflate, gzip, br\n",
- std::nullopt,
{SourceStreamType::kDeflate, SourceStreamType::kGzip,
SourceStreamType::kBrotli},
"Three encodings"},
- {"HTTP/1.1 200 OK\nContent-Encoding: deflate, gzip, br\n",
- base::flat_set<SourceStreamType>(
- {SourceStreamType::kDeflate, SourceStreamType::kBrotli}),
- {},
- "Three encodings, two accepted"},
};
for (const auto& test_case : kTestCases) {
@@ -670,8 +638,7 @@
auto headers = base::MakeRefCounted<HttpResponseHeaders>(
net::HttpUtil::AssembleRawHeaders(test_case.headers));
std::vector<SourceStreamType> types =
- FilterSourceStream::GetContentEncodingTypes(
- test_case.accepted_stream_types, *headers);
+ FilterSourceStream::GetContentEncodingTypes(*headers);
EXPECT_THAT(types, test_case.expected_result);
}
}
Original Bug Report
Site Isolation Bypass via Renderer-Controlled devtools_accepted_stream_types
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: A compromised renderer can potentially bypass Site Isolation and read credentialed cross-origin response bodies by exploiting a lack of validation on the devtools_accepted_stream_types parameter. By setting this renderer-writable Mojom field to an empty array, the network service’s content decoding is bypassed, causing raw compressed bytes to be sent to the Opaque Response Blocking (ORB) sniffing layer. Because ORB fails open on compressed data that does not match plaintext signature rules, the credentialed cross-origin response is allowed and delivered to the compromised renderer to be decompressed locally.
Affected files:
services/network/cors/cors_url_loader_factory.ccservices/network/url_loader_util.ccservices/network/public/mojom/url_request.mojomnet/filter/filter_source_stream.ccnet/url_request/url_request_http_job.ccservices/network/url_loader.ccservices/network/orb/orb_impl.cc
Estimated timestamp from git blame: 2021-03-22
Summary
A potential vulnerability in Chromium’s network service allows a compromised renderer to bypass Site Isolation (Opaque Response Blocking - ORB) and read credentialed cross-origin response bodies. The issue stems from a lack of validation on the devtools_accepted_stream_types parameter inside ResourceRequest. An untrusted renderer can set this field to an empty array, which disables content decompression in the network service. Consequently, raw compressed bytes are passed to the ORB sniffer. Since compressed bytes do not match plaintext signatures of blocked types (such as HTML, XML, or JSON), ORB fails open, sending the compressed response directly to the compromised renderer where it can be decompressed locally.
Affected Files
services/network/cors/cors_url_loader_factory.ccservices/network/url_loader_util.ccservices/network/public/mojom/url_request.mojomnet/filter/filter_source_stream.ccnet/url_request/url_request_http_job.ccservices/network/url_loader.ccservices/network/orb/orb_impl.cc
Technical Analysis
-
Lack of Validation in IsValidRequest: The
ResourceRequest.devtools_accepted_stream_typesMojom field is renderer-writable and is not stored inTrustedUrlRequestParams. InCorsURLLoaderFactory::IsValidRequest()(services/network/cors/cors_url_loader_factory.cc), no validation or sanitization is applied to this field for untrusted renderers (whereis_trusted_is false). The field is copied verbatim to thenet::URLRequestinservices/network/url_loader_util.cc(line 539):url_request.set_accepted_stream_types(request.devtools_accepted_stream_types); -
Content-Decoder Elision: When a renderer supplies
devtools_accepted_stream_types = [](an empty array) and the server returns a compressed response (e.g.,Content-Encoding: gzip):FilterSourceStream::GetContentEncodingTypes()(net/filter/filter_source_stream.ccat line 110) is called with the empty set of accepted stream types.- Because the list of accepted types is non-null but empty, the method filters out the encoding and returns an empty vector of encoding types.
URLRequestHttpJob::SetUpSourceStream()(net/url_request/url_request_http_job.ccat line 1541) callsFilterSourceStream::CreateDecodingSourceStream()with an empty vector, which returns the original, raw compressed stream without wrapping it in a decompression filter (e.g.,GzipSourceStream).- Since
client_side_content_decoding_enabledis keptfalseby the renderer, thePartialDecodersafety net inURLLoaderis never instantiated.
-
ORB Fail-Open on Compressed Bytes: When ORB is enabled, a cross-origin, no-cors response with a sniffable MIME type (such as
text/plainorapplication/json) withoutX-Content-Type-Options: nosnifffalls back to body sniffing viaOpaqueResponseBlockingAnalyzer::Sniff():- The raw compressed bytes (e.g., gzip starting with
1f 8b 08) are analyzed. - Standard plaintext sniffers (
SniffForHTML(),SniffForXML(),SniffForFetchOnlyResource()) fail to match the raw compressed byte signatures and returnkNo. - Consequently, ORB returns
kSniffMoreand eventually fails open tokAllowviaHandleEndOfSniffableResponseBody()once sniffing limits are reached. - The raw compressed bytes are piped over the Mojo data pipe directly to the renderer, which decompresses them locally to read the cross-origin authenticated data.
- The raw compressed bytes (e.g., gzip starting with
Potential Trigger Steps
Note: These are potential steps. Our tooling agent does not have the ability to execute code and has not run a proof of concept.
- A compromised renderer initiates a cross-origin authenticated request (
mode = kNoCors,credentials_mode = kInclude) targeting a compressed endpoint. - The renderer sets
devtools_accepted_stream_types = [](empty array) andclient_side_content_decoding_enabled = falseinsideResourceRequestover its boundURLLoaderFactoryMojo pipe. - The renderer includes a custom
Accept-Encoding: gzipheader to force the server to compress the response. - The network service forwards the request to the server, and the server returns a
200response containing a compressed credentialed secret. - The decoding chain is elided, bypassing decompression in the network service.
- ORB processes the raw compressed body, fails to detect JSON/HTML/XML content signatures, and fails open to
kAllow. - The compromised renderer receives the compressed payload and decodes it locally to extract the sensitive data.
Recommended Remediation
Restrict or clear devtools_accepted_stream_types in CorsURLLoaderFactory::IsValidRequest() when is_trusted_ is false, ensuring that untrusted renderers cannot modify the accepted stream types to bypass content decoding in the network service.
Evaluated with Chrome root at commit: a2bea94528f4bd6cc57739c43fa3bb890b8367d3
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.