Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in ORB
DescriptionInappropriate implementation in ORB
ComponentORB
Bug ClassLogic Error
Tracker497529290
Fix commit74c46d0bc1dc (chromium/src) +27/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-05

Changed Functions

FunctionChangeNotes
if
services/network/orb/orb_sniffers.cc
modified
TEST
services/network/orb/orb_sniffers_unittest.cc
modified

Files Changed

  • services/network/orb/orb_sniffers.cc
  • services/network/orb/orb_sniffers_unittest.cc
From 74c46d0bc1dcfcd14f5725f93a7aa61b0eb79ee1 Mon Sep 17 00:00:00 2001
From: Lukasz Anforowicz <lukasza@chromium.org>
Date: Wed, 01 Apr 2026 09:22:23 -0700
Subject: [PATCH] Teach ORB about UTF-8 BOM (byte order mark).

On one hand, ORB sniffing is a best effort heuristic.  OTOH, recognizing
and skipping the UTF-8 BOM is a fairly low hanging fruit, so it seems
worth implementing and landing this change.

Fixed: 497529290
Change-Id: I5c11865628a45cecf19afe2ff6773cb0a2d58cf9
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7717765
Reviewed-by: Charlie Reis <creis@chromium.org>
Commit-Queue: Łukasz Anforowicz <lukasza@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1608590}
---

diff --git a/services/network/orb/orb_sniffers.cc b/services/network/orb/orb_sniffers.cc
index 11dd17562..d3e27a0 100644
--- a/services/network/orb/orb_sniffers.cc
+++ b/services/network/orb/orb_sniffers.cc
@@ -25,6 +25,15 @@
 
 namespace {
 
+void AdvancePastUtf8Bom(std::string_view* data) {
+  // https://en.wikipedia.org/wiki/Byte_order_mark#UTF-8
+  const std::string_view kUtf8Bom("\xEF\xBB\xBF");
+
+  if (data->starts_with(kUtf8Bom)) {
+    data->remove_prefix(kUtf8Bom.size());
+  }
+}
+
 void AdvancePastWhitespace(std::string_view* data) {
   size_t offset = data->find_first_not_of(" \t\r\n");
   if (offset == std::string_view::npos) {
@@ -169,6 +178,7 @@
       std::string_view("<p")   // Mozilla
   };
 
+  AdvancePastUtf8Bom(&data);
   while (data.length() > 0) {
     AdvancePastWhitespace(&data);
 
@@ -192,6 +202,7 @@
   // TODO(dsjang): Once CrossOriginReadBlocking is moved into the browser
   // process, we should do single-thread checking here for the static
   // initializer.
+  AdvancePastUtf8Bom(&data);
   AdvancePastWhitespace(&data);
   static constexpr std::string_view kXmlSignatures[] = {
       std::string_view("<?xml")};
@@ -216,6 +227,7 @@
     kRightQuoteState,
   } state = kStartState;
 
+  AdvancePastUtf8Bom(&data);
   for (size_t i = 0; i < data.length(); ++i) {
     const char c = data[i];
     if (state != kLeftQuoteState && state != kEscapeState) {
@@ -297,6 +309,7 @@
       std::string_view("for (;;);"),
       std::string_view("while (1);"),
   };
+  AdvancePastUtf8Bom(&data);
   SniffingResult has_parser_breaker = MatchesSignature(
       &data, kScriptBreakingPrefixes, base::CompareCase::SENSITIVE);
   if (has_parser_breaker != kNo) {
diff --git a/services/network/orb/orb_sniffers_unittest.cc b/services/network/orb/orb_sniffers_unittest.cc
index e88ec13..507142d 100644
--- a/services/network/orb/orb_sniffers_unittest.cc
+++ b/services/network/orb/orb_sniffers_unittest.cc
@@ -108,6 +108,10 @@
   EXPECT_EQ(SniffingResult::kMaybe, SniffForHTML("<!-- unterminated..."));
   EXPECT_EQ(SniffingResult::kMaybe,
             SniffForHTML("<!-- blah --> <html> no newline yet"));
+
+  // UTF-8 BOM (https://en.wikipedia.org/wiki/Byte_order_mark#UTF-8) followed by
+  // valid HTML tags.
+  EXPECT_EQ(SniffingResult::kYes, SniffForHTML("\xEF\xBB\xBF<html><body>"));
 }
 
 TEST(OrbSnifferTest, SniffForXML) {
@@ -121,6 +125,11 @@
 
   // Empty string should be indeterminate.
   EXPECT_EQ(SniffingResult::kMaybe, SniffForXML(empty_data));
+
+  // UTF-8 BOM (https://en.wikipedia.org/wiki/Byte_order_mark#UTF-8) followed by
+  // valid XML tags.
+  EXPECT_EQ(SniffingResult::kYes,
+            SniffForXML("\xEF\xBB\xBF<?xml version=\"1.0\"?>\n <catalog"));
 }
 
 TEST(OrbSnifferTest, SniffForJSON) {
@@ -169,6 +178,11 @@
       << "Lists dictionary are not recognized (since they're valid JS too)";
   EXPECT_EQ(SniffingResult::kNo, SniffForJSON(R"({":"})"))
       << "A colon character inside a string does not trigger a match";
+
+  // UTF-8 BOM (https://en.wikipedia.org/wiki/Byte_order_mark#UTF-8) followed by
+  // valid JSON.
+  EXPECT_EQ(SniffingResult::kYes,
+            SniffForJSON("\xEF\xBB\xBF   { \"name\" : \"chrome\", "));
 }
 
 }  // namespace network::orb
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/services/network/orb/orb_sniffers_unittest.cc b/services/network/orb/orb_sniffers_unittest.cc
index e88ec13..507142d 100644
--- a/services/network/orb/orb_sniffers_unittest.cc
+++ b/services/network/orb/orb_sniffers_unittest.cc
@@ -108,6 +108,10 @@
   EXPECT_EQ(SniffingResult::kMaybe, SniffForHTML("<!-- unterminated..."));
   EXPECT_EQ(SniffingResult::kMaybe,
             SniffForHTML("<!-- blah --> <html> no newline yet"));
+
+  // UTF-8 BOM (https://en.wikipedia.org/wiki/Byte_order_mark#UTF-8) followed by
+  // valid HTML tags.
+  EXPECT_EQ(SniffingResult::kYes, SniffForHTML("\xEF\xBB\xBF<html><body>"));
 }
 
 TEST(OrbSnifferTest, SniffForXML) {
@@ -121,6 +125,11 @@
 
   // Empty string should be indeterminate.
   EXPECT_EQ(SniffingResult::kMaybe, SniffForXML(empty_data));
+
+  // UTF-8 BOM (https://en.wikipedia.org/wiki/Byte_order_mark#UTF-8) followed by
+  // valid XML tags.
+  EXPECT_EQ(SniffingResult::kYes,
+            SniffForXML("\xEF\xBB\xBF<?xml version=\"1.0\"?>\n <catalog"));
 }
 
 TEST(OrbSnifferTest, SniffForJSON) {
@@ -169,6 +178,11 @@
       << "Lists dictionary are not recognized (since they're valid JS too)";
   EXPECT_EQ(SniffingResult::kNo, SniffForJSON(R"({":"})"))
       << "A colon character inside a string does not trigger a match";
+
+  // UTF-8 BOM (https://en.wikipedia.org/wiki/Byte_order_mark#UTF-8) followed by
+  // valid JSON.
+  EXPECT_EQ(SniffingResult::kYes,
+            SniffForJSON("\xEF\xBB\xBF   { \"name\" : \"chrome\", "));
 }
 
 }  // namespace network::orb
Loading diff…

Original Bug Report

reported by vm...@google.com

ORB bypass via UTF-8 BOM allowing cross-site data leak

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: ORB’s content sniffers do not skip the UTF-8 Byte Order Mark (BOM) when analyzing responses. This causes ORB to fail to identify BOM-prefixed HTML, XML, or JSON, leading to a fail-open condition where the sensitive response is allowed into the renderer process. An attacker could exploit this to leak cross-origin data using Spectre-style attacks.

Affected files:

  • services/network/orb/orb_sniffers.cc
  • services/network/orb/orb_impl.cc

Estimated timestamp from git blame: 2024-02-20

Summary

Opaque Response Blocking (ORB) aims to prevent sensitive cross-origin data (like HTML, XML, or JSON) from entering a renderer process unless permitted by CORS. When a response lacks the X-Content-Type-Options: nosniff header, ORB relies on content sniffing to determine if the response should be blocked. However, the ORB sniffers (SniffForHTML, SniffForXML, SniffForJSON in services/network/orb/orb_sniffers.cc) fail to account for the UTF-8 Byte Order Mark (BOM, 0xEF 0xBB 0xBF) at the beginning of a response.

Technical Details

  1. Whitespace Skipping: The function AdvancePastWhitespace() is used by SniffForHTML and SniffForXML to skip leading characters, but it only checks for ASCII whitespace (' ', '\t', '\r', '\n'). It does not strip the UTF-8 BOM.
  2. Sniffer Failures:
    • SniffForHTML compares the data against signatures starting with <. Since the data starts with 0xEF, it returns kNo.
    • SniffForXML compares against <?xml and also fails.
    • SniffForJSON skips whitespace byte-by-byte but expects the first non-whitespace character to be {. Upon encountering 0xEF, it returns kNo.
  3. Fail-Open Behavior: When all sniffers fail to recognize the content, OpaqueResponseBlockingAnalyzer::Sniff() eventually falls back to HandleEndOfSniffableResponseBody(). Due to a transitional fail-open policy for unrecognized non-JS MIME types, this function returns Decision::kAllow, permitting the response to cross the process boundary.

Potential Exploitation Steps

Note: These are suggested steps based on code analysis; a working Proof of Concept has not yet been executed.

  1. An attacker creates a malicious webpage (e.g., https://evil.com).
  2. The attacker’s page executes a no-cors cross-origin request (e.g., via fetch or an <img src="..."> tag) targeting a sensitive endpoint on https://victim.com/secret.
  3. The victim server responds with sensitive HTML, XML, or JSON data. The response lacks the X-Content-Type-Options: nosniff header and the body is prefixed with a UTF-8 BOM (0xEF 0xBB 0xBF).
  4. The Network Service’s ORB implementation intercepts the response and attempts to sniff the content.
  5. Because AdvancePastWhitespace() and the sniffers do not skip the BOM, they fail to identify the response as HTML, XML, or JSON.
  6. ORB falls back to HandleEndOfSniffableResponseBody(), which returns Decision::kAllow, allowing the sensitive data into the attacker’s renderer process.
  7. The attacker utilizes a Spectre-style side-channel gadget in their JavaScript to speculatively read the sensitive cross-origin bytes from the renderer’s memory and exfiltrate them.

Suggested Fix

Modify the sniffing logic in services/network/orb/orb_sniffers.cc to explicitly detect and skip the UTF-8 BOM (0xEF 0xBB 0xBF) if it is present at the very beginning of the response body.

  • Update AdvancePastWhitespace() (or create a new helper) to strip the BOM.
  • Ensure SniffForJSON() also skips the BOM before its state machine begins evaluating characters.

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.

View on issue tracker