High chrome Cross Origin 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in CORS
DescriptionInappropriate implementation in CORS
ComponentCORS
Bug ClassCross Origin
Tracker516715010
Fix commit8781bebfa7f1 (chromium/src) +4/-51
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-18

Changed Functions

FunctionChangeNotes
TEST
net/filter/filter_source_stream_unittest.cc
modified
for
net/filter/filter_source_stream_unittest.cc
modified

Files Changed

  • net/filter/filter_source_stream.cc
  • net/filter/filter_source_stream.h
  • net/filter/filter_source_stream_unittest.cc
  • net/url_request/url_request_http_job.cc
From 8781bebfa7f1f687e854496d14fe071c587a54c1 Mon Sep 17 00:00:00 2001
From: Alex Rudenko <alexrudenko@chromium.org>
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 <caseq@chromium.org>
Reviewed-by: Tsuyoshi Horo <horo@chromium.org>
Commit-Queue: Alex Rudenko <alexrudenko@chromium.org>
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/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);
   }
 }
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;
Loading diff…

Regression Test / PoC

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

Original Bug Report

reported by vm...@google.com

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.cc
  • services/network/url_loader_util.cc
  • services/network/public/mojom/url_request.mojom
  • net/filter/filter_source_stream.cc
  • net/url_request/url_request_http_job.cc
  • services/network/url_loader.cc
  • services/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.cc
  • services/network/url_loader_util.cc
  • services/network/public/mojom/url_request.mojom
  • net/filter/filter_source_stream.cc
  • net/url_request/url_request_http_job.cc
  • services/network/url_loader.cc
  • services/network/orb/orb_impl.cc

Technical Analysis

  1. Lack of Validation in IsValidRequest: The ResourceRequest.devtools_accepted_stream_types Mojom field is renderer-writable and is not stored in TrustedUrlRequestParams. In CorsURLLoaderFactory::IsValidRequest() (services/network/cors/cors_url_loader_factory.cc), no validation or sanitization is applied to this field for untrusted renderers (where is_trusted_ is false). The field is copied verbatim to the net::URLRequest in services/network/url_loader_util.cc (line 539):

    url_request.set_accepted_stream_types(request.devtools_accepted_stream_types);
    
  2. 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.cc at 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.cc at line 1541) calls FilterSourceStream::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_enabled is kept false by the renderer, the PartialDecoder safety net in URLLoader is never instantiated.
  3. ORB Fail-Open on Compressed Bytes: When ORB is enabled, a cross-origin, no-cors response with a sniffable MIME type (such as text/plain or application/json) without X-Content-Type-Options: nosniff falls back to body sniffing via OpaqueResponseBlockingAnalyzer::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 return kNo.
    • Consequently, ORB returns kSniffMore and eventually fails open to kAllow via HandleEndOfSniffableResponseBody() 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.

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.

  1. A compromised renderer initiates a cross-origin authenticated request (mode = kNoCors, credentials_mode = kInclude) targeting a compressed endpoint.
  2. The renderer sets devtools_accepted_stream_types = [] (empty array) and client_side_content_decoding_enabled = false inside ResourceRequest over its bound URLLoaderFactory Mojo pipe.
  3. The renderer includes a custom Accept-Encoding: gzip header to force the server to compress the response.
  4. The network service forwards the request to the server, and the server returns a 200 response containing a compressed credentialed secret.
  5. The decoding chain is elided, bypassing decompression in the network service.
  6. ORB processes the raw compressed body, fails to detect JSON/HTML/XML content signatures, and fails open to kAllow.
  7. The compromised renderer receives the compressed payload and decodes it locally to extract the sensitive data.

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.

View on issue tracker