Medium watch CVSS 3.1 chrome Authorization Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
3.1
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in SiteIsolation
DescriptionIncorrect authorization in SiteIsolation
ComponentSiteIsolation
Bug ClassAuthorization Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
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.
Tracker517606780
Fix commitf61dcd7ebcda (chromium/src) +81/-9
CWECWE-863
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-05-28 (per vendor advisory)
Fix landed2026-07-20 (public git)
Disclosed2026-08-25
Patch gap36 days — fix public in source before the advisory

Background

Initiator origin
The origin of the document that started a navigation; the browser validates the renderer-supplied initiator so a compromised renderer cannot spoof who initiated a navigation.
Opaque origin precursor
An opaque origin carries a “precursor” tuple recording the origin it was derived from; validation must ensure the renderer cannot claim an arbitrary precursor it was never allowed to host.
MHTML subframe
A subframe within a saved MHTML archive document, which loads content from the archive and, historically, could have precursor origins not matching the MHTML document’s process lock.
`ChildProcessSecurityPolicyImpl` committed-origin tracking
AddCommittedOrigin() records every committed frame’s origin and HostsOrigin() consults that recorded set to decide whether a process is allowed to host a given origin.

Root Cause Analysis

VerifyInitiatorOrigin() in ipc_utils.cc contained a special case: when the navigated frame was an MHTML subframe (current_rfh && current_rfh->IsMhtmlSubframe()), it return trued early for any opaque initiator origin, accepting whatever precursor tuple the renderer supplied without further checks. As the removed comment explains, this exception dated to before ChildProcessSecurityPolicyImpl tracked committed origins, when HostsOrigin() compared only against the process lock and would have wrongly rejected the legitimate archive-derived precursors seen in NavigationMhtmlBrowserTest.DataIframe (e.g. lock file:/// sandboxed vs precursor http://8.8.8.8/). Once committed-origin tracking landed, every frame in an MHTML page that could legitimately initiate such a navigation has already committed in the process, so its opaque origin is recorded by AddCommittedOrigin and the subsequent HostsOrigin() check accepts it — making the early return unnecessary and dangerous, because it let a compromised renderer claim an opaque initiator with an arbitrary, unrelated precursor.

The fix deletes the early return so MHTML-subframe initiators fall through to the normal HostsOrigin()-based validation. The added browser test injects an opaque initiator with an unrelated precursor on an MHTML subframe’s BeginNavigation and confirms the renderer is terminated with INVALID_INITIATOR_ORIGIN.

Key insight
The core mistake was an obsolete MHTML-subframe exception that bypassed initiator-precursor validation entirely; removing it lets the now-authoritative committed-origin tracking in HostsOrigin() validate MHTML-subframe initiators like any other, rejecting spoofed precursors.

Attack Path

  1. Load an MHTML archive The victim navigates to an MHTML document containing a subframe, so an MHTML subframe (IsMhtmlSubframe() true) exists in the MHTML document’s process.
  2. Compromised renderer forges initiator A compromised/malicious renderer starts a navigation from the MHTML subframe and supplies an opaque initiator origin whose precursor is unrelated to anything the archive or process legitimately hosts (the test injects one derived from https://other-precursor.example).
  3. Bypassed validation (pre-patch) VerifyInitiatorOrigin() hit the IsMhtmlSubframe() early return and accepted the forged opaque initiator and its arbitrary precursor without consulting HostsOrigin().
  4. Spoofed initiator accepted The navigation proceeded with a renderer-chosen initiator/precursor the process was not authorized to claim, defeating the site-isolation initiator-origin check (inferred as to any concrete downstream security-decision abuse; the patch shows the acceptance/rejection, not a specific follow-on primitive).

Impact Assessment

A compromised renderer hosting an MHTML page could bypass initiator-origin validation for MHTML subframes and start navigations claiming an opaque initiator with an arbitrary precursor, undermining the browser’s authorization of who initiated a navigation — an incorrect-authorization issue in Site Isolation. The advisory classifies it as an AuthorizationError (“Incorrect authorization in SiteIsolation”) and does not report exploitation in the wild. The precondition is a renderer (already compromised, as the test simulates by overwriting the IPC’s initiator) driving navigation from an MHTML subframe; any escalation beyond initiator spoofing is (inferred).

Changed Functions

FunctionChangeNotes
BeginNavigationInitiatorReplacer
content/browser/navigation_mhtml_browsertest.cc
modified
if
content/browser/navigation_mhtml_browsertest.cc
modified

Files Changed

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

Audit Directions

  • Obsolete validation exceptions
    Search for early return true / skip-check branches justified by comments describing pre-refactor limitations (e.g. process-lock-only checks) that may be stale after security infrastructure changes.
  • Opaque origin precursors
    Audit all initiator- and origin-validation sites that special-case opaque origins to confirm the precursor tuple is still checked against committed-origin tracking rather than trusted from the renderer.
  • MHTML and other special document types
    Review other MHTML/sandboxed/data-URL special cases in navigation and origin validation to ensure they now rely on AddCommittedOrigin/HostsOrigin() rather than legacy process-lock comparisons.
From f61dcd7ebcda685675065f7abf556c4f16f69ed1 Mon Sep 17 00:00:00 2001
From: Alex Moshchuk <[email protected]>
Date: Mon, 20 Jul 2026 12:29:52 -0700
Subject: [PATCH] Validate opaque initiator precursors for MHTML subframes

VerifyInitiatorOrigin() returned early for any opaque initiator origin
when the navigated frame was an MHTML subframe, accepting whatever
precursor tuple the renderer supplied. The exception was added before
ChildProcessSecurityPolicyImpl tracked committed origins, when
HostsOrigin() compared only against the process lock and rejected the
legitimate archive-derived precursors seen in
NavigationMhtmlBrowserTest.DataIframe.

Now that AddCommittedOrigin() records every committed frame's origin and
HostsOrigin() consults that set, any frame in an MHTML page that could
legitimately initiate a navigation already passes the check. Drop the
early return so the precursor is validated like any other initiator
origin, and add a browser test that intercepts BeginNavigation on an
MHTML subframe and injects an opaque initiator with an unrelated
precursor, expecting the renderer to be terminated with
INVALID_INITIATOR_ORIGIN.

Bug: 517606780
Change-Id: I5d0f9b921d5df73fc4c32db8347baa0c2b7945c4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8118708
Commit-Queue: Alex Moshchuk <[email protected]>
Reviewed-by: Łukasz Anforowicz <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1664853}
---
diff --git a/content/browser/renderer_host/ipc_utils.cc b/content/browser/renderer_host/ipc_utils.cc
index 94ee783..9c5c718 100644
--- a/content/browser/renderer_host/ipc_utils.cc
+++ b/content/browser/renderer_host/ipc_utils.cc
@@ -100,17 +100,20 @@
       return true;
     }
 
-    // Certain (e.g., data:) navigations in subframes of MHTML documents may
-    // have precursor origins that do not match the process lock of the MHTML
-    // document. This is seen in NavigationMhtmlBrowserTest.DataIframe, where:
+    // Navigations in subframes of MHTML documents may have precursor origins
+    // that do not match the process lock of the MHTML document. This is seen
+    // in NavigationMhtmlBrowserTest.DataIframe, where:
     //   - renderer origin lock = { file:/// sandboxed }
     //   - precursor of initiator origin = http://8.8.8.8/
-    // Note that RenderFrameHostImpl::CanCommitOriginAndUrl() similarly allows
-    // such navigations to commit, and it also ensures that they can only commit
-    // in the main frame MHTML document's process.
-    if (current_rfh && current_rfh->IsMhtmlSubframe()) {
-      return true;
-    }
+    // In the past, this case used to be special-cased here, but this is no
+    // longer needed now that ChildProcessSecurityPolicy's enforcements have
+    // been switched to use committed origin tracking. Any frame in the MHTML
+    // page that could legitimately initiate such a navigation has already
+    // committed in this process, so its (opaque) origin has been recorded by
+    // ChildProcessSecurityPolicyImpl::AddCommittedOrigin and the HostsOrigin()
+    // check below will accept it. There is therefore no need to skip the check
+    // for MHTML subframes, and doing so would allow the renderer to claim an
+    // opaque initiator with an arbitrary precursor.
   }
 
   auto* policy = ChildProcessSecurityPolicyImpl::GetInstance();
diff --git a/content/browser/navigation_mhtml_browsertest.cc b/content/browser/navigation_mhtml_browsertest.cc
index df1989b..7126186 100644
--- a/content/browser/navigation_mhtml_browsertest.cc
+++ b/content/browser/navigation_mhtml_browsertest.cc
@@ -30,6 +30,7 @@
 #include "content/public/test/test_utils.h"
 #include "content/shell/browser/shell.h"
 #include "content/test/content_browser_test_utils_internal.h"
+#include "content/test/frame_host_interceptor.h"
 #include "mojo/public/c/system/trap.h"
 #include "mojo/public/c/system/types.h"
 #include "mojo/public/cpp/system/data_pipe.h"
@@ -41,6 +42,7 @@
 #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/origin.h"
 #include "url/url_constants.h"
 
 namespace content {
@@ -1022,4 +1024,71 @@
       kill_waiter.Wait());
 }
 
+// Verifies that an MHTML subframe cannot start a navigation with an opaque
+// initiator origin whose precursor doesn't correspond to anything that has
+// committed in the MHTML document's process.
+IN_PROC_BROWSER_TEST_F(NavigationMhtmlImprovementsBrowserTest,
+                       MhtmlSubframeBeginNavigationOpaqueInitiatorPrecursor) {
+  // Intercepts BeginNavigation to overwrite the initiator origin once
+  // activated. This simulates a renderer that lies about the initiator.
+  class BeginNavigationInitiatorReplacer : public FrameHostInterceptor {
+   public:
+    BeginNavigationInitiatorReplacer(WebContents* web_contents,
+                                     url::Origin initiator_to_inject)
+        : FrameHostInterceptor(web_contents),
+          initiator_to_inject_(std::move(initiator_to_inject)) {}
+
+    bool WillDispatchBeginNavigation(
+        RenderFrameHost* render_frame_host,
+        blink::mojom::CommonNavigationParamsPtr* common_params,
+        blink::mojom::BeginNavigationParamsPtr* begin_params,
+        mojo::PendingRemote<blink::mojom::BlobURLToken>* blob_url_token,
+        mojo::PendingAssociatedRemote<mojom::NavigationClient>*
+            navigation_client) override {
+      if (is_activated_) {
+        (*common_params)->initiator_origin = initiator_to_inject_;
+        is_activated_ = false;
+      }
+      return true;
+    }
+
+    void Activate() { is_activated_ = true; }
+
+   private:
+    url::Origin initiator_to_inject_;
+    bool is_activated_ = false;
+  };
+
+  // The interceptor must be created before the frames whose IPCs it will
+  // intercept. Use an opaque origin whose precursor is unrelated to anything
+  // the MHTML archive contains.
+  url::Origin injected_origin =
+      url::Origin::Create(GURL("https://other-precursor.example"))
+          .DeriveNewOpaqueOrigin();
+  BeginNavigationInitiatorReplacer injector(web_contents(), injected_origin);
+
+  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();
+  ASSERT_TRUE(sub_document->IsMhtmlSubframe());
+
+  // Start a renderer-initiated navigation in the subframe and overwrite its
+  // initiator origin. The renderer should be terminated since the precursor
+  // doesn't match anything the process is allowed to host.
+  RenderProcessHostBadIpcMessageWaiter kill_waiter(sub_document->GetProcess());
+  injector.Activate();
+  ExecuteScriptAsync(sub_document, "window.location = 'about:blank';");
+  EXPECT_EQ(bad_message::INVALID_INITIATOR_ORIGIN, kill_waiter.Wait());
+}
+
 }  // namespace content
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 df1989b..7126186 100644
--- a/content/browser/navigation_mhtml_browsertest.cc
+++ b/content/browser/navigation_mhtml_browsertest.cc
@@ -30,6 +30,7 @@
 #include "content/public/test/test_utils.h"
 #include "content/shell/browser/shell.h"
 #include "content/test/content_browser_test_utils_internal.h"
+#include "content/test/frame_host_interceptor.h"
 #include "mojo/public/c/system/trap.h"
 #include "mojo/public/c/system/types.h"
 #include "mojo/public/cpp/system/data_pipe.h"
@@ -41,6 +42,7 @@
 #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/origin.h"
 #include "url/url_constants.h"
 
 namespace content {
@@ -1022,4 +1024,71 @@
       kill_waiter.Wait());
 }
 
+// Verifies that an MHTML subframe cannot start a navigation with an opaque
+// initiator origin whose precursor doesn't correspond to anything that has
+// committed in the MHTML document's process.
+IN_PROC_BROWSER_TEST_F(NavigationMhtmlImprovementsBrowserTest,
+                       MhtmlSubframeBeginNavigationOpaqueInitiatorPrecursor) {
+  // Intercepts BeginNavigation to overwrite the initiator origin once
+  // activated. This simulates a renderer that lies about the initiator.
+  class BeginNavigationInitiatorReplacer : public FrameHostInterceptor {
+   public:
+    BeginNavigationInitiatorReplacer(WebContents* web_contents,
+                                     url::Origin initiator_to_inject)
+        : FrameHostInterceptor(web_contents),
+          initiator_to_inject_(std::move(initiator_to_inject)) {}
+
+    bool WillDispatchBeginNavigation(
+        RenderFrameHost* render_frame_host,
+        blink::mojom::CommonNavigationParamsPtr* common_params,
+        blink::mojom::BeginNavigationParamsPtr* begin_params,
+        mojo::PendingRemote<blink::mojom::BlobURLToken>* blob_url_token,
+        mojo::PendingAssociatedRemote<mojom::NavigationClient>*
+            navigation_client) override {
+      if (is_activated_) {
+        (*common_params)->initiator_origin = initiator_to_inject_;
+        is_activated_ = false;
+      }
+      return true;
+    }
+
+    void Activate() { is_activated_ = true; }
+
+   private:
+    url::Origin initiator_to_inject_;
+    bool is_activated_ = false;
+  };
+
+  // The interceptor must be created before the frames whose IPCs it will
+  // intercept. Use an opaque origin whose precursor is unrelated to anything
+  // the MHTML archive contains.
+  url::Origin injected_origin =
+      url::Origin::Create(GURL("https://other-precursor.example"))
+          .DeriveNewOpaqueOrigin();
+  BeginNavigationInitiatorReplacer injector(web_contents(), injected_origin);
+
+  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();
+  ASSERT_TRUE(sub_document->IsMhtmlSubframe());
+
+  // Start a renderer-initiated navigation in the subframe and overwrite its
+  // initiator origin. The renderer should be terminated since the precursor
+  // doesn't match anything the process is allowed to host.
+  RenderProcessHostBadIpcMessageWaiter kill_waiter(sub_document->GetProcess());
+  injector.Activate();
+  ExecuteScriptAsync(sub_document, "window.location = 'about:blank';");
+  EXPECT_EQ(bad_message::INVALID_INITIATOR_ORIGIN, kill_waiter.Wait());
+}
+
 }  // namespace content
Loading diff…

Original Bug Report

reported by [email protected]

VerifyInitiatorOrigin bypass via MHTML subframe opaque initiator origin

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 security bypass in VerifyInitiatorOrigin potentially allows a compromised MHTML renderer process to skip process-lock validation checks on opaque initiator origins for MHTML subframes. By supplying an opaque origin containing an arbitrary, attacker-forged precursor origin, the renderer can successfully commit a navigation with the spoofed precursor. This weakness allows the compromised process to register the forged precursor origin within the ChildProcessSecurityPolicy’s committed origins list, bypassing subsequent Site Isolation defenses.

Affected files:

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

Estimated timestamp from git blame: 2024-06-14

Root Cause Analysis

In content/browser/renderer_host/ipc_utils.cc, VerifyInitiatorOrigin is responsible for ensuring that a renderer process is authorized to host the initiator origin of a navigation. However, the function contains an early return that short-circuits the check if the initiator origin is opaque and the target frame is an MHTML subframe:

// content/browser/renderer_host/ipc_utils.cc
if (initiator_origin.opaque()) {
  ...
  if (current_rfh && current_rfh->IsMhtmlSubframe()) {
    return true;
  }
}
auto* policy = ChildProcessSecurityPolicyImpl::GetInstance();
if (!policy->HostsOrigin(process_id, initiator_origin)) {
  ...
}

Because the browser returns true early on line 112, it skips the subsequent HostsOrigin check. This allows a compromised MHTML process to supply an opaque origin wrapping an arbitrary, forged precursor origin (such as https://victim.example) without triggering a process termination.

During navigation commit, RenderFrameHostImpl::CanCommitOriginAndUrl similarly allows opaque origins to commit in an MHTML subframe if it resides in the same SiteInstance as the main frame, which also skips standard process-lock checks on the precursor tuple.

Upon commit, RenderFrameHostImpl::UpdatePermissionsForNavigation registers the committed origin via ChildProcessSecurityPolicyImpl::AddCommittedOrigin, adding opaque{precursor=https://victim.example} to the process’s committed_origins_ list. This enables the compromised process to pass subsequent HostsOrigin checks for the spoofed precursor origin, allowing it to bypass cross-origin boundaries (such as spoofing the source origin in postMessage calls).


Potential Steps to Trigger the Vulnerability

Note: These steps are based on static code analysis; our analysis does not currently include a running proof of concept.

  1. A user loads a local or downloaded MHTML archive containing an <iframe> subframe S.
  2. The compromised renderer process hosting the MHTML document sends a FrameHost::BeginNavigation IPC targeting subframe S with:
    • common_params.url = "about:blank"
    • common_params.initiator_origin set to an opaque origin with a forged precursor of https://victim.example.
  3. VerifyInitiatorOrigin detects IsMhtmlSubframe() is true and accepts the forged initiator origin without validation.
  4. Since the URL is about:blank, the browser derives the commit origin directly from the initiator origin, preserving the forged precursor.
  5. The navigation completes. ChildProcessSecurityPolicy registers the origin opaque{precursor=https://victim.example} within the process’s committed origins.
  6. The compromised renderer then calls RouteMessageEvent (postMessage) targeting https://victim.example with the spoofed source origin, which successfully passes the HostsOrigin checks in RenderFrameProxyHost.

Suggested Remediation

To remediate this issue, the browser should ensure that the precursor of any opaque initiator origin supplied by an MHTML subframe is validated against the process lock of the MHTML renderer process instead of being skipped. VerifyInitiatorOrigin should check that the precursor of the initiator origin is compatible with the process lock, or enforce that the precursor matches the main frame’s origin (or is otherwise authorized), rather than returning true unconditionally.

Evaluated with Chrome root at commit: 5133b93d189b383c37805b1cf3a9d2dbfe8d7379


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.

View on issue tracker