CVE-2026-17779
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
whilethird_party/blink/renderer/platform/network/http_parsers.cc |
modified | |
TESTthird_party/blink/renderer/platform/network/http_parsers_test.cc |
modified |
Files Changed
net/base/mime_util.hthird_party/blink/renderer/platform/network/http_parsers.ccthird_party/blink/renderer/platform/network/http_parsers_test.cc
Patch
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")));
Regression Test / PoC
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.
Original Bug Report
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.ccthird_party/blink/renderer/platform/network/http_parsers.ccservices/network/orb/orb_impl.ccnet/http/http_util.ccnet/http/http_stream_parser.ccnet/spdy/spdy_http_utils.ccthird_party/blink/renderer/platform/loader/fetch/resource_response.ccthird_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
-
Network Process (Last-wins): In
net/http/http_response_headers.cc, theGetMimeTypeAndCharsetmethod iterates through allContent-Typeheaders. BecauseContent-Typeis not marked as a non-coalescing header, the network stack splits comma-separated values and processes them sequentially. TheHttpUtil::ParseContentTypemethod overwrites themime_typeresult 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. -
Blink Renderer (First-wins): When headers are synchronized to the renderer, Blink’s
ExtractMIMETypeFromMediaType(inthird_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. -
Lack of Validation: Unlike
Content-LengthorLocation, the network stack does not currently enforce that multipleContent-Typeheaders 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::CheckResponseNosniffwill block the load because the MIME type is nottext/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
- 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 - From an attacker origin, attempt to load this resource as a stylesheet:
<link rel="stylesheet" href="...">. - Monitor the load via
chrome://net-exportto confirm that ORB allows the response (Decision::kAllow) due to thetext/cssclassification. - Confirm via the DevTools console that Blink blocks the stylesheet because its internal parsing identifies the type as
application/json. - At this point, the JSON data resides in the renderer’s memory, bypassing ORB’s isolation guarantee.
Suggested Fix
- Align Blink’s
ExtractMIMETypeFromMediaTypewith the Fetch specification’s ’last-wins’ approach for MIME type extraction. - Consider implementing a check in
net/http/http_stream_parser.ccornet/spdy/spdy_http_utils.ccto reject responses with multiple inconsistentContent-Typeheaders, similar to the existing checks forContent-LengthandLocation.
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.