Medium chrome OOB 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in Offline
DescriptionOut of bounds read in Offline
ComponentOffline
Sub-componentOffline Pages
Bug ClassOOB
Tracker514040146
Fix commit370fb330e153 (chromium/src) +102/-19
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
if
chrome/browser/offline_pages/offline_page_tab_helper.cc
modified
TEST_F
chrome/browser/offline_pages/offline_page_tab_helper_unittest.cc
modified

Files Changed

  • chrome/browser/offline_pages/offline_page_tab_helper_unittest.cc
  • chrome/browser/offline_pages/offline_page_tab_helper.cc
From 370fb330e15375c712ecb822d833c5cb4e90bf27 Mon Sep 17 00:00:00 2001
From: Carlos Knippschild <[email protected]>
Date: Fri, 18 Sep 2026 09:43:58 -0700
Subject: [PATCH] [Offline Pages] Prevent origin spoofing for untrusted MHTML files

Ensure untrusted MHTML archives loaded via file:// or content:// cannot
spoof the origin displayed in Page Info on Android.

Previously, OfflinePageTabHelper set offline_page->url to the unverified
Content-Location header URL reported by the renderer. On Android, Page
Info displayed this URL as the page's origin and title. Now, untrusted
archives always use the navigated local URL.

Changes:
- Use the navigated URL as offline_page->url for untrusted offline pages.
- Reject mismatched renderer-reported URLs for HTTP/HTTPS navigations.
- Add unit tests for untrusted local archives and HTTP/HTTPS matching.

Fixed: 514040146
Change-Id: I2a4d160b7eab314bbc62bde0a00be73b6a6a6964
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8422981
Reviewed-by: Dan H <[email protected]>
Auto-Submit: Carlos Knippschild <[email protected]>
Commit-Queue: Dan H <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1700868}
---
diff --git a/chrome/browser/offline_pages/offline_page_tab_helper.cc b/chrome/browser/offline_pages/offline_page_tab_helper.cc
index 419b8836..30de9b7c 100644
--- a/chrome/browser/offline_pages/offline_page_tab_helper.cc
+++ b/chrome/browser/offline_pages/offline_page_tab_helper.cc
@@ -45,8 +45,9 @@
 namespace {
 bool SchemeIsForUntrustedOfflinePages(const GURL& url) {
 #if BUILDFLAG(IS_ANDROID)
-  if (url.SchemeIs(url::kContentScheme))
+  if (url.SchemeIs(url::kContentScheme)) {
     return true;
+  }
 #endif
   return url.SchemeIsFile();
 }
@@ -71,11 +72,13 @@
         receiver,
     content::RenderFrameHost* rfh) {
   auto* web_contents = content::WebContents::FromRenderFrameHost(rfh);
-  if (!web_contents)
+  if (!web_contents) {
     return;
+  }
   auto* tab_helper = OfflinePageTabHelper::FromWebContents(web_contents);
-  if (!tab_helper)
+  if (!tab_helper) {
     return;
+  }
   tab_helper->mhtml_page_notifier_receivers_.Bind(rfh, std::move(receiver));
 }
 
@@ -150,22 +153,26 @@
   }
 
   // Sanity checking the input URL.
-  if (!main_frame_url.is_valid() || !main_frame_url.SchemeIsHTTPOrHTTPS())
+  if (!main_frame_url.is_valid() || !main_frame_url.SchemeIsHTTPOrHTTPS()) {
     return;
+  }
 
-  if (!provisional_offline_info_.IsValid())
+  if (!provisional_offline_info_.IsValid()) {
     provisional_offline_info_ = LoadedOfflinePageInfo::MakeUntrusted();
+  }
   provisional_offline_info_.offline_page->url = main_frame_url;
 
-  if (!date.is_null())
+  if (!date.is_null()) {
     provisional_offline_info_.offline_page->creation_time = date;
+  }
 }
 
 void OfflinePageTabHelper::DidStartNavigation(
     content::NavigationHandle* navigation_handle) {
   // Skips non-main frame.
-  if (!navigation_handle->IsInPrimaryMainFrame())
+  if (!navigation_handle->IsInPrimaryMainFrame()) {
     return;
+  }
 
   // The provisional offline info can be cleared no matter how.
   provisional_offline_info_.Clear();
@@ -174,14 +181,17 @@
 void OfflinePageTabHelper::DidFinishNavigation(
     content::NavigationHandle* navigation_handle) {
   // Skips non-main frame.
-  if (!navigation_handle->IsInPrimaryMainFrame())
+  if (!navigation_handle->IsInPrimaryMainFrame()) {
     return;
+  }
 
-  if (!navigation_handle->HasCommitted())
+  if (!navigation_handle->HasCommitted()) {
     return;
+  }
 
-  if (navigation_handle->IsSameDocument())
+  if (navigation_handle->IsSameDocument()) {
     return;
+  }
 
   if (offline_info_.IsValid()) {
     // Do not store the offline page we are navigating away from in bfcache.
@@ -209,8 +219,9 @@
     content::NavigationHandle* navigation_handle) {
   offline_info_.Clear();
 
-  if (navigation_handle->IsErrorPage())
+  if (navigation_handle->IsErrorPage()) {
     return;
+  }
 
   GURL navigated_url = navigation_handle->GetURL();
 
@@ -230,14 +241,18 @@
     } else {
       // Otherwise, just use an empty untrusted page.
       offline_info_ = LoadedOfflinePageInfo::MakeUntrusted();
+    }
+    if (offline_info_.trusted_state == OfflinePageTrustedState::UNTRUSTED) {
       offline_info_.offline_page->url = navigated_url;
     }
   } else if (navigated_url.SchemeIsHTTPOrHTTPS()) {
     // For http/https URL, commit the provisional offline info if any.
     if (provisional_offline_info_.IsValid()) {
-      DCHECK(EqualsIgnoringFragment(
-          navigated_url, provisional_offline_info_.offline_page->url));
-      offline_info_ = std::move(provisional_offline_info_);
+      if (EqualsIgnoringFragment(navigated_url,
+                                 provisional_offline_info_.offline_page->url)) {
+        offline_info_ = std::move(provisional_offline_info_);
+        offline_info_.offline_page->url = navigated_url;
+      }
       provisional_offline_info_.Clear();
     }
   }
@@ -247,14 +262,16 @@
     content::NavigationHandle* navigation_handle) {
   // If the offline page has been loaded successfully, nothing more to do.
   net::Error error_code = navigation_handle->GetNetErrorCode();
-  if (error_code == net::OK)
+  if (error_code == net::OK) {
     return;
+  }
 
   // We might be reloading the URL in order to fetch the offline page.
   // * If successful, nothing to do.
   // * Otherwise, we're hitting error again. Bail out to avoid loop.
-  if (reloading_url_on_net_error_)
+  if (reloading_url_on_net_error_) {
     return;
+  }
 
   // When the navigation starts, the request might be intercepted to serve the
   // offline content if the network is detected to be in disconnected or poor
@@ -353,10 +370,12 @@
 }
 
 const OfflinePageItem* OfflinePageTabHelper::GetOfflinePreviewItem() const {
-  if (provisional_offline_info_.is_showing_offline_preview)
+  if (provisional_offline_info_.is_showing_offline_preview) {
     return provisional_offline_info_.offline_page.get();
-  if (offline_info_.is_showing_offline_preview)
+  }
+  if (offline_info_.is_showing_offline_preview) {
     return offline_info_.offline_page.get();
+  }
   return nullptr;
 }
 
@@ -409,8 +428,9 @@
   offline_pages::RequestCoordinator* request_coordinator =
       offline_pages::RequestCoordinatorFactory::GetForBrowserContext(
           web_contents->GetBrowserContext());
-  if (!request_coordinator)
+  if (!request_coordinator) {
     return;
+  }
 
   offline_pages::RequestCoordinator::SavePageLaterParams params;
   params.url = url;
diff --git a/chrome/browser/offline_pages/offline_page_tab_helper_unittest.cc b/chrome/browser/offline_pages/offline_page_tab_helper_unittest.cc
index c79b664d..572efc7 100644
--- a/chrome/browser/offline_pages/offline_page_tab_helper_unittest.cc
+++ b/chrome/browser/offline_pages/offline_page_tab_helper_unittest.cc
@@ -393,5 +393,68 @@
   EXPECT_EQ(tab_helper()->GetOfflinePreviewItem(), offline_page_item);
 }
 
+TEST_F(OfflinePageTabHelperTest, UntrustedMhtmlFileLoadOriginNotSpoofed) {
+  const GURL kFileUrl("file:///sdcard/Download/GoogleSignIn.mhtml");
+  const GURL kSpoofedUrl("https://accounts.google.com/signin");
+
+  tab_helper()->SetCurrentTargetFrameForTest(
+      web_contents()->GetPrimaryMainFrame());
+
+  CreateNavigationSimulator(kFileUrl);
+  navigation_simulator()->Start();
+
+  navigation_simulator()->SetContentsMimeType("multipart/related");
+  tab_helper()->NotifyMhtmlPageLoadAttempted(
+      MHTMLLoadResult::kSuccess, kSpoofedUrl, kTestMhtmlCreationTime);
+  navigation_simulator()->Commit();
+
+  ASSERT_NE(nullptr, tab_helper()->offline_page());
+  EXPECT_EQ(OfflinePageTrustedState::UNTRUSTED, tab_helper()->trusted_state());
+  EXPECT_FALSE(tab_helper()->IsShowingTrustedOfflinePage());
+  EXPECT_EQ(kTestMhtmlCreationTime,
+            tab_helper()->offline_page()->creation_time);
+  // Offline page URL must reflect the actual navigated file URL, preventing
+  // origin spoofing in Page Info and open-online reload.
+  EXPECT_EQ(kFileUrl, tab_helper()->offline_page()->url);
+}
+
+TEST_F(OfflinePageTabHelperTest, HttpMhtmlLoadMismatchedUrlRejected) {
+  const GURL kNavigatedUrl("https://example.com/page.html");
+  const GURL kMismatchedUrl("https://evil.com/phish.html");
+
+  tab_helper()->SetCurrentTargetFrameForTest(
+      web_contents()->GetPrimaryMainFrame());
+
+  CreateNavigationSimulator(kNavigatedUrl);
+  navigation_simulator()->Start();
+
+  navigation_simulator()->SetContentsMimeType("multipart/related");
+  tab_helper()->NotifyMhtmlPageLoadAttempted(
+      MHTMLLoadResult::kSuccess, kMismatchedUrl, kTestMhtmlCreationTime);
+  navigation_simulator()->Commit();
+
+  // Mismatched renderer-reported URL for HTTP/HTTPS navigation must be
+  // rejected.
+  EXPECT_EQ(nullptr, tab_helper()->offline_page());
+}
+
+TEST_F(OfflinePageTabHelperTest, HttpMhtmlLoadMatchingUrlAccepted) {
+  const GURL kNavigatedUrl("https://example.com/page.html");
+
+  tab_helper()->SetCurrentTargetFrameForTest(
+      web_contents()->GetPrimaryMainFrame());
+
+  CreateNavigationSimulator(kNavigatedUrl);
+  navigation_simulator()->Start();
+
+  navigation_simulator()->SetContentsMimeType("multipart/related");
+  tab_helper()->NotifyMhtmlPageLoadAttempted(
+      MHTMLLoadResult::kSuccess, kNavigatedUrl, kTestMhtmlCreationTime);
+  navigation_simulator()->Commit();
+
+  ASSERT_NE(nullptr, tab_helper()->offline_page());
+  EXPECT_EQ(kNavigatedUrl, tab_helper()->offline_page()->url);
+}
+
 }  // namespace
 }  // namespace offline_pages
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/offline_pages/offline_page_tab_helper_unittest.cc b/chrome/browser/offline_pages/offline_page_tab_helper_unittest.cc
index c79b664d..572efc7 100644
--- a/chrome/browser/offline_pages/offline_page_tab_helper_unittest.cc
+++ b/chrome/browser/offline_pages/offline_page_tab_helper_unittest.cc
@@ -393,5 +393,68 @@
   EXPECT_EQ(tab_helper()->GetOfflinePreviewItem(), offline_page_item);
 }
 
+TEST_F(OfflinePageTabHelperTest, UntrustedMhtmlFileLoadOriginNotSpoofed) {
+  const GURL kFileUrl("file:///sdcard/Download/GoogleSignIn.mhtml");
+  const GURL kSpoofedUrl("https://accounts.google.com/signin");
+
+  tab_helper()->SetCurrentTargetFrameForTest(
+      web_contents()->GetPrimaryMainFrame());
+
+  CreateNavigationSimulator(kFileUrl);
+  navigation_simulator()->Start();
+
+  navigation_simulator()->SetContentsMimeType("multipart/related");
+  tab_helper()->NotifyMhtmlPageLoadAttempted(
+      MHTMLLoadResult::kSuccess, kSpoofedUrl, kTestMhtmlCreationTime);
+  navigation_simulator()->Commit();
+
+  ASSERT_NE(nullptr, tab_helper()->offline_page());
+  EXPECT_EQ(OfflinePageTrustedState::UNTRUSTED, tab_helper()->trusted_state());
+  EXPECT_FALSE(tab_helper()->IsShowingTrustedOfflinePage());
+  EXPECT_EQ(kTestMhtmlCreationTime,
+            tab_helper()->offline_page()->creation_time);
+  // Offline page URL must reflect the actual navigated file URL, preventing
+  // origin spoofing in Page Info and open-online reload.
+  EXPECT_EQ(kFileUrl, tab_helper()->offline_page()->url);
+}
+
+TEST_F(OfflinePageTabHelperTest, HttpMhtmlLoadMismatchedUrlRejected) {
+  const GURL kNavigatedUrl("https://example.com/page.html");
+  const GURL kMismatchedUrl("https://evil.com/phish.html");
+
+  tab_helper()->SetCurrentTargetFrameForTest(
+      web_contents()->GetPrimaryMainFrame());
+
+  CreateNavigationSimulator(kNavigatedUrl);
+  navigation_simulator()->Start();
+
+  navigation_simulator()->SetContentsMimeType("multipart/related");
+  tab_helper()->NotifyMhtmlPageLoadAttempted(
+      MHTMLLoadResult::kSuccess, kMismatchedUrl, kTestMhtmlCreationTime);
+  navigation_simulator()->Commit();
+
+  // Mismatched renderer-reported URL for HTTP/HTTPS navigation must be
+  // rejected.
+  EXPECT_EQ(nullptr, tab_helper()->offline_page());
+}
+
+TEST_F(OfflinePageTabHelperTest, HttpMhtmlLoadMatchingUrlAccepted) {
+  const GURL kNavigatedUrl("https://example.com/page.html");
+
+  tab_helper()->SetCurrentTargetFrameForTest(
+      web_contents()->GetPrimaryMainFrame());
+
+  CreateNavigationSimulator(kNavigatedUrl);
+  navigation_simulator()->Start();
+
+  navigation_simulator()->SetContentsMimeType("multipart/related");
+  tab_helper()->NotifyMhtmlPageLoadAttempted(
+      MHTMLLoadResult::kSuccess, kNavigatedUrl, kTestMhtmlCreationTime);
+  navigation_simulator()->Commit();
+
+  ASSERT_NE(nullptr, tab_helper()->offline_page());
+  EXPECT_EQ(kNavigatedUrl, tab_helper()->offline_page()->url);
+}
+
 }  // namespace
 }  // namespace offline_pages
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.