CVE-2026-9903
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
IN_PROC_BROWSER_TEST_Fcontent/browser/navigation_mhtml_browsertest.cc |
modified |
Files Changed
content/browser/navigation_mhtml_browsertest.cccontent/browser/renderer_host/render_frame_host_impl.cc
Patch
From e4b92a699c36c550e02209491591b664293f20f1 Mon Sep 17 00:00:00 2001 From: Arthur Sonzogni <[email protected]> Date: Wed, 13 May 2026 20:01:26 -0700 Subject: [PATCH] Fix Site Isolation bypass via MHTML subframe origin spoofing RenderFrameHostImpl::CanCommitOriginAndUrl contained an early-return short-circuit for MHTML subframes that unconditionally accepted the renderer-provided origin without validating it. This could allow a compromised renderer to spoof its origin via a fabricated same-document navigation IPC. This CL fixes the issue by explicitly enforcing that MHTML subframes must commit with opaque origins at the early-return block, mirroring the error-page enforcement at ValidateDidCommitParams(). It also adds a regression test that simulates a compromised renderer sending a malicious DidCommitSameDocumentNavigation IPC with a non-opaque origin. Fixed: 498783665 Change-Id: I8b09fdfa418d4d9846d94ab18a7e5f904d94d983 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7837898 Auto-Submit: Arthur Sonzogni <[email protected]> Commit-Queue: Rakina Zata Amni <[email protected]> Reviewed-by: Rakina Zata Amni <[email protected]> Cr-Commit-Position: refs/heads/main@{#1630370} --- diff --git a/content/browser/navigation_mhtml_browsertest.cc b/content/browser/navigation_mhtml_browsertest.cc index 7a5e12c..682db42 100644 --- a/content/browser/navigation_mhtml_browsertest.cc +++ b/content/browser/navigation_mhtml_browsertest.cc @@ -15,10 +15,12 @@ #include "base/test/bind.h" #include "base/test/scoped_feature_list.h" #include "base/threading/thread_restrictions.h" +#include "content/browser/bad_message.h" #include "content/browser/renderer_host/navigation_request.h" #include "content/browser/renderer_host/render_frame_host_impl.h" #include "content/browser/web_contents/web_contents_impl.h" #include "content/common/content_navigation_policy.h" +#include "content/common/frame.mojom.h" #include "content/public/common/url_constants.h" #include "content/public/test/browser_test.h" #include "content/public/test/browser_test_utils.h" @@ -37,6 +39,7 @@ #include "net/dns/mock_host_resolver.h" #include "services/network/public/cpp/web_sandbox_flags.h" #include "third_party/blink/public/common/features.h" +#include "third_party/blink/public/common/page_state/page_state.h" #include "url/gurl.h" #include "url/url_constants.h" @@ -465,6 +468,52 @@ EXPECT_EQ(0u, sub_document->child_count()); } +IN_PROC_BROWSER_TEST_F(NavigationMhtmlBrowserTest, + MhtmlSubframeSameDocumentOriginSpoof) { + MhtmlArchive mhtml_archive; + mhtml_archive.AddHtmlDocument( + GURL("http://example.com"), + "<iframe src=\"http://example.com/subframe.html\"></iframe>"); + mhtml_archive.AddHtmlDocument(GURL("http://example.com/subframe.html"), + "subframe content"); + GURL mhtml_url = mhtml_archive.Write("index.mhtml"); + + EXPECT_TRUE(NavigateToURL(shell(), mhtml_url)); + + RenderFrameHostImpl* main_document = main_frame_host(); + ASSERT_EQ(1u, main_document->child_count()); + RenderFrameHostImpl* sub_document = + main_document->child_at(0)->current_frame_host(); + + EXPECT_TRUE(main_document->is_mhtml_document()); + EXPECT_TRUE(sub_document->is_mhtml_document()); + EXPECT_TRUE(sub_document->GetLastCommittedOrigin().opaque()); + + // Simulate a compromised renderer sending a malicious + // DidCommitSameDocumentNavigation IPC with a non-opaque origin. + auto params = mojom::DidCommitProvisionalLoadParams::New(); + params->url = GURL("https://victim.example/#poc"); + params->origin = url::Origin::Create(GURL("https://victim.example")); + params->navigation_token = base::UnguessableToken::Create(); + // Fill in other required params to avoid other validation failures. + params->did_create_new_entry = false; + params->method = "GET"; + params->page_state = blink::PageState::CreateFromURL(params->url); + params->transition = ui::PAGE_TRANSITION_AUTO_SUBFRAME; + params->referrer = blink::mojom::Referrer::New(); + + auto same_doc_params = mojom::DidCommitSameDocumentNavigationParams::New(); + + // We expect the renderer to be killed. + RenderProcessHostBadIpcMessageWaiter kill_waiter(sub_document->GetProcess()); + + static_cast<mojom::FrameHost*>(sub_document) + ->DidCommitSameDocumentNavigation(std::move(params), + std::move(same_doc_params)); + + EXPECT_EQ(bad_message::RFH_INVALID_ORIGIN_ON_COMMIT, kill_waiter.Wait()); +} + // Load iframe with the content-ID scheme. The resource is found in the MHTML // archive. IN_PROC_BROWSER_TEST_F(NavigationMhtmlBrowserTest, IframeContentIdFound) { diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc index bdf5447..171bee2 100644 --- a/content/browser/renderer_host/render_frame_host_impl.cc +++ b/content/browser/renderer_host/render_frame_host_impl.cc @@ -11771,6 +11771,15 @@ // (e.g. "http://localhost"). In such cases, don't verify the URL, but require // the URL to commit in the process of the main frame. if (IsMhtmlSubframe()) { + // Documents derived from an MHTML archive are behind sandbox flags, so + // their origin is opaque. The early-return below validates neither URL + // nor origin, so a compromised renderer could otherwise launder an + // arbitrary non-opaque origin past this point via + // DidCommitSameDocumentNavigation. + if (!origin.opaque()) { + LogCanCommitOriginAndUrlFailureReason("mhtml_subframe_non_opaque_origin"); + return CanCommitStatus::CANNOT_COMMIT_ORIGIN; + } RenderFrameHostImpl* main_frame = GetMainFrame(); if (IsSameSiteInstance(main_frame)) { return CanCommitStatus::CAN_COMMIT_ORIGIN_AND_URL;
Regression Test / PoC
diff --git a/content/browser/navigation_mhtml_browsertest.cc b/content/browser/navigation_mhtml_browsertest.cc
index 7a5e12c..682db42 100644
--- a/content/browser/navigation_mhtml_browsertest.cc
+++ b/content/browser/navigation_mhtml_browsertest.cc
@@ -15,10 +15,12 @@
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/threading/thread_restrictions.h"
+#include "content/browser/bad_message.h"
#include "content/browser/renderer_host/navigation_request.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/common/content_navigation_policy.h"
+#include "content/common/frame.mojom.h"
#include "content/public/common/url_constants.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
@@ -37,6 +39,7 @@
#include "net/dns/mock_host_resolver.h"
#include "services/network/public/cpp/web_sandbox_flags.h"
#include "third_party/blink/public/common/features.h"
+#include "third_party/blink/public/common/page_state/page_state.h"
#include "url/gurl.h"
#include "url/url_constants.h"
@@ -465,6 +468,52 @@
EXPECT_EQ(0u, sub_document->child_count());
}
+IN_PROC_BROWSER_TEST_F(NavigationMhtmlBrowserTest,
+ MhtmlSubframeSameDocumentOriginSpoof) {
+ MhtmlArchive mhtml_archive;
+ mhtml_archive.AddHtmlDocument(
+ GURL("http://example.com"),
+ "<iframe src=\"http://example.com/subframe.html\"></iframe>");
+ mhtml_archive.AddHtmlDocument(GURL("http://example.com/subframe.html"),
+ "subframe content");
+ GURL mhtml_url = mhtml_archive.Write("index.mhtml");
+
+ EXPECT_TRUE(NavigateToURL(shell(), mhtml_url));
+
+ RenderFrameHostImpl* main_document = main_frame_host();
+ ASSERT_EQ(1u, main_document->child_count());
+ RenderFrameHostImpl* sub_document =
+ main_document->child_at(0)->current_frame_host();
+
+ EXPECT_TRUE(main_document->is_mhtml_document());
+ EXPECT_TRUE(sub_document->is_mhtml_document());
+ EXPECT_TRUE(sub_document->GetLastCommittedOrigin().opaque());
+
+ // Simulate a compromised renderer sending a malicious
+ // DidCommitSameDocumentNavigation IPC with a non-opaque origin.
+ auto params = mojom::DidCommitProvisionalLoadParams::New();
+ params->url = GURL("https://victim.example/#poc");
+ params->origin = url::Origin::Create(GURL("https://victim.example"));
+ params->navigation_token = base::UnguessableToken::Create();
+ // Fill in other required params to avoid other validation failures.
+ params->did_create_new_entry = false;
+ params->method = "GET";
+ params->page_state = blink::PageState::CreateFromURL(params->url);
+ params->transition = ui::PAGE_TRANSITION_AUTO_SUBFRAME;
+ params->referrer = blink::mojom::Referrer::New();
+
+ auto same_doc_params = mojom::DidCommitSameDocumentNavigationParams::New();
+
+ // We expect the renderer to be killed.
+ RenderProcessHostBadIpcMessageWaiter kill_waiter(sub_document->GetProcess());
+
+ static_cast<mojom::FrameHost*>(sub_document)
+ ->DidCommitSameDocumentNavigation(std::move(params),
+ std::move(same_doc_params));
+
+ EXPECT_EQ(bad_message::RFH_INVALID_ORIGIN_ON_COMMIT, kill_waiter.Wait());
+}
+
// Load iframe with the content-ID scheme. The resource is found in the MHTML
// archive.
IN_PROC_BROWSER_TEST_F(NavigationMhtmlBrowserTest, IframeContentIdFound) {
Original Bug Report
Site Isolation bypass via MHTML subframe origin spoofing in CanCommitOriginAndUrl
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 without the security team.
Overview: A vulnerability in RenderFrameHostImpl::CanCommitOriginAndUrl allows a compromised renderer hosting an MHTML subframe to spoof its origin. By sending a fabricated same-document navigation IPC, the renderer can bypass origin validation and arbitrarily overwrite the browser’s last_committed_origin_. This can lead to privilege escalation, allowing the attacker to abuse permissions or WebAuthn credentials associated with the spoofed origin.
Affected files:
content/browser/renderer_host/render_frame_host_impl.cc
Estimated timestamp from git blame: 2024-06-13
Description
A potential Site Isolation bypass exists in the browser’s origin validation logic for MHTML subframes. RenderFrameHostImpl::CanCommitOriginAndUrl contains an early-return short-circuit for MHTML subframes that unconditionally accepts the renderer-provided origin without validating it.
When combined with the fact that features::kEnforceSameDocumentOriginInvariants is currently disabled by default, a compromised renderer can exploit this short-circuit to forge a same-document navigation IPC containing an arbitrary cross-origin origin. The browser process will accept this IPC and update its authoritative last_committed_origin_ for the frame to the attacker’s spoofed origin.
Technical Details
In content/browser/renderer_host/render_frame_host_impl.cc, the CanCommitOriginAndUrl function short-circuits validation for MHTML subframes:
if (IsMhtmlSubframe()) {
RenderFrameHostImpl* main_frame = GetMainFrame();
if (IsSameSiteInstance(main_frame)) {
return CanCommitStatus::CAN_COMMIT_ORIGIN_AND_URL;
}
// ...
}
This bypasses several critical security checks, including the ChildProcessSecurityPolicyImpl enforcement. Furthermore, the fallback check for same-document origin changes (which operates when kEnforceSameDocumentOriginInvariants is disabled) is positioned after this MHTML block, meaning the origin invariance check is completely skipped.
When a malicious same-document navigation IPC passes this validation, execution eventually reaches RenderFrameHostImpl::DidNavigate. Because kEnforceSameDocumentOriginInvariants is disabled, the browser unconditionally executes SetLastCommittedOrigin(params.origin) for same-document navigations.
Once last_committed_origin_ is corrupted, the attacker can silently abuse any browser-side APIs that rely on it, such as permission requests (camera/mic/location) or WebAuthn assertions for the victim origin.
Potential Attack Steps
Note: Our tooling agent cannot run code, so these are potential steps to trigger the vulnerability based on static analysis.
- The victim opens a local MHTML file, causing the browser to load a main frame and subframe in the same
SiteInstancewithis_mhtml_document_set to true. - The attacker gains code execution in the renderer process (e.g., via a memory corruption bug in an image or font decoder, as JS is disabled in MHTML by default).
- The compromised renderer constructs a malicious
FrameHost::DidCommitSameDocumentNavigationIPC targeted at the subframe. - The IPC parameters include a spoofed origin (e.g.,
https://victim.example) and a random, unrecognizednavigation_token. - The browser processes the IPC. The unknown token causes it to synthesize a
NavigationRequest, but validation relies onCanCommitOriginAndUrl. - The MHTML short-circuit in
CanCommitOriginAndUrlunconditionally accepts the spoofed origin. RenderFrameHostImpl::DidNavigatecallsSetLastCommittedOrigin(params.origin), successfully spoofing the browser process’s state.
Suggested Fix
Move the fallback same-document origin check (which runs when kEnforceSameDocumentOriginInvariants is disabled) to occur before the IsMhtmlSubframe() early return in CanCommitOriginAndUrl. Alternatively, explicitly enforce that MHTML subframes must commit with opaque origins in RenderFrameHostImpl::ValidateDidCommitParams(), similar to how error pages are currently validated.
Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33
Results 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.