High watch ✔︎ verified CVSS 5 chrome Input Validation 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Site Isolation
DescriptionInsufficient validation of untrusted input in Site Isolation
ComponentSite Isolation
Bug ClassInput Validation
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety — factors set by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIConfirmed
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker498783665
Fix commite4b92a699c36 (chromium/src) +58/-0
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:L
CISA KEVNot listed
CreditedGoogle
Reported2026-04-02 (per vendor advisory)
Fix landed2026-05-14 (public git)
Disclosed2026-05-27
Patch gap13 days — fix public in source before the advisory

Background

`RenderFrameHostImpl::CanCommitOriginAndUrl`
the browser-process gate that decides whether a renderer may commit a given origin/URL in a frame (render_frame_host_impl.cc:11725).
MHTML subframe
a subframe loaded from an MHTML archive; IsMhtmlSubframe() is true when the frame is not the main frame and the main frame is an MHTML document (render_frame_host_impl.cc:11721).
Opaque origin
an origin with no tuple (scheme/host/port) that is distinct from every other origin; MHTML-derived documents are sandboxed and expected to be opaque.
`kEnforceSameDocumentOriginInvariants`
a feature flag; when disabled, the stronger same-document origin-invariance check is skipped in favor of a weaker fallback.

Root Cause Analysis

CanCommitOriginAndUrl runs a series of checks, but the strong same-document origin check only runs when features::IsEnforceSameDocumentOriginInvariantsEnabled() is true (render_frame_host_impl.cc:11762-11766). For MHTML subframes, the function then entered a dedicated block (render_frame_host_impl.cc:11773) that, when the subframe is in the same SiteInstance as the main frame, returned CanCommitStatus::CAN_COMMIT_ORIGIN_AND_URL unconditionally (render_frame_host_impl.cc:11775-11776) — accepting the renderer-supplied origin without validating it. Critically, the weaker fallback same-document origin check sits after this block (render_frame_host_impl.cc:11798-11803) and a comment at render_frame_host_impl.cc:11793-11794 notes it “must be after the MHTML check,” so a same-document commit for an MHTML subframe skipped origin validation entirely and never reached ChildProcessSecurityPolicyImpl::CanCommitOriginAndUrl (render_frame_host_impl.cc:11814). The invariant that an MHTML subframe commits only with its expected opaque origin was thus unenforced.

The fix adds, at the top of the IsMhtmlSubframe() block (added diff at @@ -11771,6), if (!origin.opaque()) { LogCanCommitOriginAndUrlFailureReason("mhtml_subframe_non_opaque_origin"); return CanCommitStatus::CANNOT_COMMIT_ORIGIN; }, so a non-opaque origin on an MHTML subframe is rejected before the permissive early return — tightening the check. The regression test confirms the misbehaving renderer is killed with bad_message::RFH_INVALID_ORIGIN_ON_COMMIT.

Key insight
The MHTML-subframe early-return in CanCommitOriginAndUrl accepted the renderer’s origin without checking it, bypassing the same-document origin-invariance check that follows; the fix requires MHTML subframe origins to be opaque() at that early return and rejects anything else.

Attack Path

  1. Load an MHTML archive The victim opens a local MHTML file so a main frame and subframe load in the same SiteInstance as MHTML documents. (trigger per reporter)
  2. Compromise the renderer The attacker gains code execution in that renderer process. (inferred; not shown in code)
  3. Forge a same-document commit The renderer sends a DidCommitSameDocumentNavigation IPC for the subframe carrying a spoofed non-opaque origin (e.g. https://victim.example), as the regression test demonstrates.
  4. Origin accepted pre-fix CanCommitOriginAndUrl hits the MHTML block and returns CAN_COMMIT_ORIGIN_AND_URL without validating the origin (render_frame_host_impl.cc:11775-11776), so the browser adopts the spoofed origin.
  5. Abuse of spoofed origin The browser’s authoritative committed origin for the frame is now the attacker-chosen origin, enabling abuse of origin-keyed browser APIs. (inferred; SetLastCommittedOrigin path not in shown material)

Impact Assessment

A compromised renderer hosting an MHTML subframe could spoof the browser process’s authoritative origin for that frame via a forged same-document navigation IPC, a Site Isolation bypass that could let it abuse permissions or credentials bound to the spoofed origin. The advisory rates this high severity with no in-the-wild exploitation; the privilege-escalation consequences are (inferred) beyond the origin acceptance shown.

Changed Functions

FunctionChangeNotes
IN_PROC_BROWSER_TEST_F
content/browser/navigation_mhtml_browsertest.cc
modified

Files Changed

  • content/browser/navigation_mhtml_browsertest.cc
  • content/browser/renderer_host/render_frame_host_impl.cc

Audit Directions

  • Permissive early returns in commit validation
    Any branch of CanCommitOriginAndUrl (or similar commit gates) that returns “can commit” early must itself validate origin/URL, since later checks are skipped.
  • Checks ordered after a short-circuit
    When a security check is deliberately placed after a special-case block (here the MHTML block preceding the fallback origin check), confirm the special case cannot be the exact path that needs that check.
  • Expected-opaque origins
    For documents required to be sandboxed/opaque (MHTML, error pages), enforce the opaque invariant explicitly at commit time rather than assuming the renderer honors it.
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;
Loading diff…

Regression Test / PoC

shipped with the fix
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) {
Loading diff…

Original Bug Report

reported by [email protected]

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.

  1. The victim opens a local MHTML file, causing the browser to load a main frame and subframe in the same SiteInstance with is_mhtml_document_ set to true.
  2. 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).
  3. The compromised renderer constructs a malicious FrameHost::DidCommitSameDocumentNavigation IPC targeted at the subframe.
  4. The IPC parameters include a spoofed origin (e.g., https://victim.example) and a random, unrecognized navigation_token.
  5. The browser processes the IPC. The unknown token causes it to synthesize a NavigationRequest, but validation relies on CanCommitOriginAndUrl.
  6. The MHTML short-circuit in CanCommitOriginAndUrl unconditionally accepts the spoofed origin.
  7. RenderFrameHostImpl::DidNavigate calls SetLastCommittedOrigin(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.

View on issue tracker