Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Site Isolation
DescriptionInappropriate implementation in Site Isolation
ComponentSite Isolation
Bug ClassLogic Error
Tracker513478933
Fix commitbcf7740409e9 (chromium/src) +65/-74
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Changed Functions

FunctionChangeNotes
while
third_party/blink/renderer/platform/network/http_parsers.cc
modified
TEST
third_party/blink/renderer/platform/network/http_parsers_test.cc
modified

Files Changed

  • net/base/mime_util.h
  • third_party/blink/renderer/platform/network/http_parsers.cc
  • third_party/blink/renderer/platform/network/http_parsers_test.cc
From bcf7740409e9f262144af01be09924f9c637a6dc Mon Sep 17 00:00:00 2001
From: Nidhi Jaju <[email protected]>
Date: Thu, 04 Jun 2026 02:16:02 -0700
Subject: [PATCH] Align Content-Type media-type parsing in Blink with Fetch spec

Refactors ExtractMIMETypeFromMediaType in Blink's http_parsers.cc
to reuse net::HttpUtil::ParseContentType and
net::HttpUtil::ValuesIterator.

Specifically, this:
- Aligns parsing of comma-separated Content-Type headers to
  last-wins, conforming to the Fetch specification and matching the
  network process.
- Normalizes returned MIME types to lowercase.
- Eliminates custom parsing logic in Blink to guarantee parser
  parity with the network stack.

Updated existing unit tests to match strict net::HttpUtil
validation and last-wins expectations.

Bug: 513478933
Change-Id: I60bfea213dd1affa2bfb04d734cd99ab8e6de586
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7884824
Reviewed-by: Takashi Toyoshima <[email protected]>
Commit-Queue: Nidhi Jaju <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1641539}
---

diff --git a/net/base/mime_util.h b/net/base/mime_util.h
index 33b1040..66045834 100644
--- a/net/base/mime_util.h
+++ b/net/base/mime_util.h
@@ -89,6 +89,15 @@
 // If |params| is non-NULL, clears it and sets it with name-value pairs of
 // parsed parameters. Parsing of parameters is lenient, and invalid params are
 // ignored.
+//
+// Note on invalid inputs:
+// - If the input is missing a slash, or has space/tab before the slash (e.g.
+//   "text / html"), it returns false.
+// - If the input has other invalid characters in type/subtype (like newlines or
+//   unusual spaces, e.g. "text\n/\nhtml"), but has a slash and no space/tab
+//   before it, ParseMimeType will return true and extract the whole substring
+//   including the invalid characters (since it only terminates parsing the type
+//   on spaces, tabs, semicolons, or open parenthesis).
 NET_EXPORT bool ParseMimeType(std::string_view type_str,
                               std::string* mime_type,
                               base::StringPairs* params);
diff --git a/third_party/blink/renderer/platform/network/http_parsers.cc b/third_party/blink/renderer/platform/network/http_parsers.cc
index 24d894b..3a701ad0 100644
--- a/third_party/blink/renderer/platform/network/http_parsers.cc
+++ b/third_party/blink/renderer/platform/network/http_parsers.cc
@@ -610,48 +610,32 @@
   return parsed_time;
 }
 
+// Extracts the MIME type from a Content-Type/media-type header value.
+//
+// This function delegates parsing to net::HttpUtil::ParseContentType (which
+// internally calls net::ParseMimeType) for parity with the network process.
+// See net::ParseMimeType for how invalid inputs are handled.
 AtomicString ExtractMIMETypeFromMediaType(const AtomicString& media_type) {
-  unsigned length = media_type.length();
-
-  unsigned pos = 0;
-
-  while (pos < length) {
-    UChar c = media_type[pos];
-    if (c != '\t' && c != ' ')
-      break;
-    ++pos;
-  }
-
-  if (pos == length)
+  if (media_type.empty()) {
     return media_type;
-
-  unsigned type_start = pos;
-
-  unsigned type_end = pos;
-  while (pos < length) {
-    UChar c = media_type[pos];
-
-    // While RFC 2616 does not allow it, other browsers allow multiple values in
-    // the HTTP media type header field, Content-Type. In such cases, the media
-    // type string passed here may contain the multiple values separated by
-    // commas. For now, this code ignores text after the first comma, which
-    // prevents it from simply failing to parse such types altogether.  Later
-    // for better compatibility we could consider using the first or last valid
-    // MIME type instead.
-    // See https://bugs.webkit.org/show_bug.cgi?id=25352 for more discussion.
-    if (c == ',' || c == ';')
-      break;
-
-    if (c != '\t' && c != ' ')
-      type_end = pos + 1;
-
-    ++pos;
   }
 
-  // Use a StringView to create an AtomicString here so we do not allocate an
-  // intermediate string.
-  return AtomicString(
-      StringView(media_type, type_start, type_end - type_start));
+  std::string media_type_std = media_type.Utf8();
+  std::string mime_type;
+  std::string charset;
+  bool had_charset = false;
+
+  net::HttpUtil::ValuesIterator it(media_type_std, ',',
+                                   /*ignore_empty_values=*/true);
+  while (it.GetNext()) {
+    net::HttpUtil::ParseContentType(it.value(), &mime_type, &charset,
+                                    &had_charset, /*boundary=*/nullptr);
+  }
+
+  if (mime_type.empty()) {
+    return g_empty_atom;
+  }
+  return AtomicString::FromUtf8(mime_type);
 }
 
 bool IsHTTPTabOrSpace(UChar c) {
diff --git a/third_party/blink/renderer/platform/network/http_parsers_test.cc b/third_party/blink/renderer/platform/network/http_parsers_test.cc
index c6101e01..c55441c 100644
--- a/third_party/blink/renderer/platform/network/http_parsers_test.cc
+++ b/third_party/blink/renderer/platform/network/http_parsers_test.cc
@@ -357,6 +357,7 @@
 
 TEST(HTTPParsersTest, ExtractMIMETypeFromMediaType) {
   const AtomicString text_html("text/html");
+  const AtomicString text_plain("text/plain");
 
   EXPECT_EQ(text_html, ExtractMIMETypeFromMediaType(AtomicString("text/html")));
   EXPECT_EQ(text_html, ExtractMIMETypeFromMediaType(
@@ -378,27 +379,30 @@
   EXPECT_EQ(text_html, ExtractMIMETypeFromMediaType(
                            AtomicString("text/html ; charset=iso-8859-1")));
 
-  // Non-standard multiple type/subtype listing using a comma as a separator
-  // is accepted.
-  EXPECT_EQ(text_html,
+  // Multiple type/subtype listing using a comma as a separator. The last valid
+  // entry wins.
+  EXPECT_EQ(text_plain,
             ExtractMIMETypeFromMediaType(AtomicString("text/html,text/plain")));
-  EXPECT_EQ(text_html, ExtractMIMETypeFromMediaType(
-                           AtomicString("text/html , text/plain")));
-  EXPECT_EQ(text_html, ExtractMIMETypeFromMediaType(
-                           AtomicString("text/html\t,\ttext/plain")));
-  EXPECT_EQ(text_html, ExtractMIMETypeFromMediaType(AtomicString(
-                           "text/html,text/plain;charset=iso-8859-1")));
+  EXPECT_EQ(text_plain, ExtractMIMETypeFromMediaType(
+                            AtomicString("text/html , text/plain")));
+  EXPECT_EQ(text_plain, ExtractMIMETypeFromMediaType(
+                            AtomicString("text/html\t,\ttext/plain")));
+  EXPECT_EQ(text_plain, ExtractMIMETypeFromMediaType(AtomicString(
+                            "text/html,text/plain;charset=iso-8859-1")));
 
-  // Preserves case.
-  EXPECT_EQ("tExt/hTMl",
-            ExtractMIMETypeFromMediaType(AtomicString("tExt/hTMl")));
+  // Converts to lowercase for consistency between Blink and ORB.
+  EXPECT_EQ(text_html, ExtractMIMETypeFromMediaType(AtomicString("tExt/hTMl")));
 
-  EXPECT_EQ(g_empty_string,
+  // Unusual valid and invalid MIME type declarations.
+  EXPECT_EQ(text_html,
             ExtractMIMETypeFromMediaType(AtomicString(", text/html")));
   EXPECT_EQ(g_empty_string,
             ExtractMIMETypeFromMediaType(AtomicString("; text/html")));
 
   // If no normalization is required, the same AtomicString should be returned.
+  // Note: Since net::HttpUtil converts to lowercase and returns a new
+  // AtomicString, we do not expect the same implementation pointer if it is
+  // modified. But for already lowercase "text/html", it should still match.
   const AtomicString& passthrough = ExtractMIMETypeFromMediaType(text_html);
   EXPECT_EQ(text_html.Impl(), passthrough.Impl());
 }
@@ -415,8 +419,9 @@
 
 TEST(HTTPParsersTest, ExtractMIMETypeFromMediaTypeInvalidInput) {
   // extractMIMETypeFromMediaType() returns the string before the first
-  // semicolon after trimming OWSes at the head and the tail even if the
-  // string doesn't conform to the media-type ABNF defined in the RFC 7231.
+  // semicolon after trimming OWSes (Optional White Spaces) at the head and the
+  // tail even if the string doesn't conform to the media-type ABNF defined in
+  // the RFC 7231.
 
   // These behaviors could be fixed later when ready.
 
@@ -428,8 +433,9 @@
             ExtractMIMETypeFromMediaType(
                 AtomicString::FromUtf8("\xE2\x80\x83text/html")));
 
-  // Invalid type/subtype.
-  EXPECT_EQ(AtomicString("a"), ExtractMIMETypeFromMediaType(AtomicString("a")));
+  // Invalid type/subtype is rejected because it doesn't contain a slash, so
+  // net::HttpUtil::ParseMimeType returns false, leading to g_empty_string.
+  EXPECT_EQ(g_empty_string, ExtractMIMETypeFromMediaType(AtomicString("a")));
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/renderer/platform/network/http_parsers_test.cc b/third_party/blink/renderer/platform/network/http_parsers_test.cc
index c6101e01..c55441c 100644
--- a/third_party/blink/renderer/platform/network/http_parsers_test.cc
+++ b/third_party/blink/renderer/platform/network/http_parsers_test.cc
@@ -357,6 +357,7 @@
 
 TEST(HTTPParsersTest, ExtractMIMETypeFromMediaType) {
   const AtomicString text_html("text/html");
+  const AtomicString text_plain("text/plain");
 
   EXPECT_EQ(text_html, ExtractMIMETypeFromMediaType(AtomicString("text/html")));
   EXPECT_EQ(text_html, ExtractMIMETypeFromMediaType(
@@ -378,27 +379,30 @@
   EXPECT_EQ(text_html, ExtractMIMETypeFromMediaType(
                            AtomicString("text/html ; charset=iso-8859-1")));
 
-  // Non-standard multiple type/subtype listing using a comma as a separator
-  // is accepted.
-  EXPECT_EQ(text_html,
+  // Multiple type/subtype listing using a comma as a separator. The last valid
+  // entry wins.
+  EXPECT_EQ(text_plain,
             ExtractMIMETypeFromMediaType(AtomicString("text/html,text/plain")));
-  EXPECT_EQ(text_html, ExtractMIMETypeFromMediaType(
-                           AtomicString("text/html , text/plain")));
-  EXPECT_EQ(text_html, ExtractMIMETypeFromMediaType(
-                           AtomicString("text/html\t,\ttext/plain")));
-  EXPECT_EQ(text_html, ExtractMIMETypeFromMediaType(AtomicString(
-                           "text/html,text/plain;charset=iso-8859-1")));
+  EXPECT_EQ(text_plain, ExtractMIMETypeFromMediaType(
+                            AtomicString("text/html , text/plain")));
+  EXPECT_EQ(text_plain, ExtractMIMETypeFromMediaType(
+                            AtomicString("text/html\t,\ttext/plain")));
+  EXPECT_EQ(text_plain, ExtractMIMETypeFromMediaType(AtomicString(
+                            "text/html,text/plain;charset=iso-8859-1")));
 
-  // Preserves case.
-  EXPECT_EQ("tExt/hTMl",
-            ExtractMIMETypeFromMediaType(AtomicString("tExt/hTMl")));
+  // Converts to lowercase for consistency between Blink and ORB.
+  EXPECT_EQ(text_html, ExtractMIMETypeFromMediaType(AtomicString("tExt/hTMl")));
 
-  EXPECT_EQ(g_empty_string,
+  // Unusual valid and invalid MIME type declarations.
+  EXPECT_EQ(text_html,
             ExtractMIMETypeFromMediaType(AtomicString(", text/html")));
   EXPECT_EQ(g_empty_string,
             ExtractMIMETypeFromMediaType(AtomicString("; text/html")));
 
   // If no normalization is required, the same AtomicString should be returned.
+  // Note: Since net::HttpUtil converts to lowercase and returns a new
+  // AtomicString, we do not expect the same implementation pointer if it is
+  // modified. But for already lowercase "text/html", it should still match.
   const AtomicString& passthrough = ExtractMIMETypeFromMediaType(text_html);
   EXPECT_EQ(text_html.Impl(), passthrough.Impl());
 }
@@ -415,8 +419,9 @@
 
 TEST(HTTPParsersTest, ExtractMIMETypeFromMediaTypeInvalidInput) {
   // extractMIMETypeFromMediaType() returns the string before the first
-  // semicolon after trimming OWSes at the head and the tail even if the
-  // string doesn't conform to the media-type ABNF defined in the RFC 7231.
+  // semicolon after trimming OWSes (Optional White Spaces) at the head and the
+  // tail even if the string doesn't conform to the media-type ABNF defined in
+  // the RFC 7231.
 
   // These behaviors could be fixed later when ready.
 
@@ -428,8 +433,9 @@
             ExtractMIMETypeFromMediaType(
                 AtomicString::FromUtf8("\xE2\x80\x83text/html")));
 
-  // Invalid type/subtype.
-  EXPECT_EQ(AtomicString("a"), ExtractMIMETypeFromMediaType(AtomicString("a")));
+  // Invalid type/subtype is rejected because it doesn't contain a slash, so
+  // net::HttpUtil::ParseMimeType returns false, leading to g_empty_string.
+  EXPECT_EQ(g_empty_string, ExtractMIMETypeFromMediaType(AtomicString("a")));
 
   // Invalid parameters.
   EXPECT_EQ(AtomicString("text/html"),
@@ -439,13 +445,19 @@
   EXPECT_EQ(AtomicString("text/html"),
             ExtractMIMETypeFromMediaType(AtomicString("text/html; = = = ")));
 
-  // Only OWSes at either the beginning or the end of the type/subtype
-  // portion.
-  EXPECT_EQ(AtomicString("text / html"),
+  // net::HttpUtil::ParseMimeType rejects spaces before the slash because the
+  // type/subtype portion is parsed up to the first space/tab character.
+  // Therefore "text" is treated as the MIME type and lacks a slash, so it is
+  // rejected.
+  EXPECT_EQ(g_empty_string,
             ExtractMIMETypeFromMediaType(AtomicString("text / html")));
-  EXPECT_EQ(AtomicString("t e x t / h t m l"),
+  EXPECT_EQ(g_empty_string,
             ExtractMIMETypeFromMediaType(AtomicString("t e x t / h t m l")));
 
+  // net::HttpUtil::ParseMimeType does not perform strict token validation
+  // on other invalid characters like newlines or non-standard whitespaces if
+  // they appear in the middle without hitting space, tab, semicolon or open
+  // parenthesis. Thus, these are returned as-is.
   EXPECT_EQ(AtomicString("text\r\n/\nhtml"),
             ExtractMIMETypeFromMediaType(AtomicString("text\r\n/\nhtml")));
   EXPECT_EQ(AtomicString("text\n/\nhtml"),
diff --git a/third_party/blink/web_tests/external/wpt/fetch/content-type/script.window-expected.txt b/third_party/blink/web_tests/external/wpt/fetch/content-type/script.window-expected.txt
index dafe196..86357b13 100644
--- a/third_party/blink/web_tests/external/wpt/fetch/content-type/script.window-expected.txt
+++ b/third_party/blink/web_tests/external/wpt/fetch/content-type/script.window-expected.txt
@@ -1,21 +1,7 @@
 This is a testharness.js-based test.
-[FAIL] separate x/x text/javascript
-  assert_unreached: onerror Reached unreachable code
-[FAIL] combined x/x text/javascript
-  assert_unreached: onerror Reached unreachable code
-[FAIL] separate x/x;charset=windows-1252 text/javascript
-  assert_unreached: onerror Reached unreachable code
-[FAIL] combined x/x;charset=windows-1252 text/javascript
-  assert_unreached: onerror Reached unreachable code
-[FAIL] separate text/javascript x/x
-  assert_unreached: onload Reached unreachable code
-[FAIL] combined text/javascript x/x
-  assert_unreached: onload Reached unreachable code
 [FAIL] separate text/javascript;charset=windows-1252;" \\" x/x
   assert_equals: expected "€" but got "€"
 [FAIL] separate x/x;" x/y;\\" text/javascript;charset=windows-1252;" text/javascript
-  assert_unreached: onerror Reached unreachable code
-[FAIL] combined x/x;" x/y;\\" text/javascript;charset=windows-1252;" text/javascript
-  assert_unreached: onerror Reached unreachable code
+  assert_equals: expected "€" but got "€"
 Harness: the test ran to completion.
Loading diff…

Original Bug Report

reported by [email protected]

ORB bypass via Content-Type parser differential (net/ vs Blink)

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 discrepancy between how the network process and the renderer process parse multiple Content-Type headers allows attackers to potentially bypass Opaque Response Blocking (ORB). By appending a safelisted MIME type to a sensitive response, an attacker can trick ORB into allowing cross-origin data into the renderer process memory, where it remains vulnerable to side-channel attacks.

Affected files:

  • net/http/http_response_headers.cc
  • third_party/blink/renderer/platform/network/http_parsers.cc
  • services/network/orb/orb_impl.cc
  • net/http/http_util.cc
  • net/http/http_stream_parser.cc
  • net/spdy/spdy_http_utils.cc
  • third_party/blink/renderer/platform/loader/fetch/resource_response.cc
  • third_party/blink/renderer/platform/loader/fetch/resource_loader.cc

Estimated timestamp from git blame: Unknown (Google3 checkout)

Description

There is a potential differential in how Chromium’s network stack (net/) and the Blink rendering engine extract the MIME type from HTTP responses containing multiple Content-Type values. This inconsistency can be leveraged to bypass Opaque Response Blocking (ORB), a security mechanism designed to prevent sensitive cross-origin data from entering a renderer process’s memory (as a defense against Spectre side-channel attacks).

Root Cause Analysis

  1. Network Process (Last-wins): In net/http/http_response_headers.cc, the GetMimeTypeAndCharset method iterates through all Content-Type headers. Because Content-Type is not marked as a non-coalescing header, the network stack splits comma-separated values and processes them sequentially. The HttpUtil::ParseContentType method overwrites the mime_type result for every valid segment encountered. Consequently, the network layer adopts a ’last valid segment wins’ strategy. This is the value used by ORB to make its blocking decision.

  2. Blink Renderer (First-wins): When headers are synchronized to the renderer, Blink’s ExtractMIMETypeFromMediaType (in third_party/blink/renderer/platform/network/http_parsers.cc) extracts the MIME type. However, this parser explicitly breaks at the first comma or semicolon it encounters. Thus, Blink effectively employs a ‘first valid segment wins’ strategy.

  3. Lack of Validation: Unlike Content-Length or Location, the network stack does not currently enforce that multiple Content-Type headers must be identical or unique, allowing this differential to persist.

Potential Security Impact

An attacker can exploit this by ensuring a sensitive cross-origin response (e.g., application/json) also includes a trailing safelisted MIME type (e.g., text/css).

  • ORB (Network Process): Sees the last value (text/css), classifies it as an opaque-safelisted type, and allows the response body to be delivered to the renderer process without body sniffing.
  • Blink (Renderer Process): Sees the first value (application/json). If the resource was loaded as a stylesheet (<link rel="stylesheet">), ResourceLoader::CheckResponseNosniff will block the load because the MIME type is not text/css.

Crucially, although the renderer blocks the interpretation of the data, the sensitive bytes have already crossed the process boundary and reside in the renderer’s memory. A compromised renderer or a Spectre gadget can then be used to read this data from the process address space.

Suggested Potential Reproduction Steps

  1. Set up a cross-origin server that responds with the following headers and a sensitive body:
    Content-Type: application/json
    X-Content-Type-Options: nosniff
    Content-Type: text/css
    
  2. From an attacker origin, attempt to load this resource as a stylesheet: <link rel="stylesheet" href="...">.
  3. Monitor the load via chrome://net-export to confirm that ORB allows the response (Decision::kAllow) due to the text/css classification.
  4. Confirm via the DevTools console that Blink blocks the stylesheet because its internal parsing identifies the type as application/json.
  5. At this point, the JSON data resides in the renderer’s memory, bypassing ORB’s isolation guarantee.

Suggested Fix

  1. Align Blink’s ExtractMIMETypeFromMediaType with the Fetch specification’s ’last-wins’ approach for MIME type extraction.
  2. Consider implementing a check in net/http/http_stream_parser.cc or net/spdy/spdy_http_utils.cc to reject responses with multiple inconsistent Content-Type headers, similar to the existing checks for Content-Length and Location.

Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a


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