Low watch CVSS 6.5 chrome Authorization Error 🔧 Commit mapped

Overview

Low
Severity
6.5
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.
Tracker498793976
Fix commit2ddf8c2004f3 (chromium/src) +141/-9
CWECWE-863
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-04-02 (per vendor advisory)
Fix landed2026-09-15 (public git)
Disclosed2026-09-29
Patch gap14 days — fix public in source before the advisory

Background

Synchronous about:blank commit
A commit the renderer performs locally (without a browser-side NavigationRequest) when a frame first loads about:blank, reporting its result to the browser via DidCommitProvisionalLoadParams.
`last_committed_origin_`
The browser-derived origin for the frame, set in SetOriginDependentStateOfNewFrame() before the first navigation and treated as the authoritative expected origin for the synchronous commit.
Opaque origin / precursor
An origin with no tuple (e.g. a sandboxed document), carrying a precursor tuple recording the origin it was derived from, retrievable via GetTupleOrPrecursorTupleIfOpaque().
`Origin::CanBeDerivedFrom()`
A url::Origin helper that checks whether an origin could legitimately have been produced from a given GURL, and which DCHECKs (crashes/undefined in release) when handed an invalid/empty GURL.

Root Cause Analysis

The origin-validation branch in DidCommitNavigationInternal() (in render_frame_host_impl.cc) guarded synchronous about:blank commits with !params->origin.opaque() && !params->origin.CanBeDerivedFrom(last_committed_origin_.GetURL()). This had two defects: it only ran the CanBeDerivedFrom() consistency check when the renderer-reported origin was non-opaque, so a compromised renderer could report any opaque origin and have it silently accepted, replacing the browser-derived last_committed_origin_; and when last_committed_origin_ was itself opaque, GetURL() yielded an empty/invalid GURL that was passed into CanBeDerivedFrom(), tripping its DCHECK.

The fix branches on whether the browser-derived origin is opaque: if opaque, the reported origin must also be opaque with a matching precursor (GetTupleOrPrecursorTupleIfOpaque() equality); if non-opaque, the existing CanBeDerivedFrom() check is applied but now always with a valid GURL; only on failure does it call ReceivedBadMessage() and return false. This restores the invariant that the renderer-reported origin for a synchronous about:blank commit must agree with the browser-derived origin, closing the path that let the renderer substitute an unrelated origin.

Key insight
The single core mistake was accepting any opaque origin from the renderer unconditionally (the check only validated non-opaque origins), letting a compromised renderer overwrite the browser’s authoritative origin; the fix validates the opaque case too by requiring a matching precursor, and simultaneously stops feeding an invalid GURL into CanBeDerivedFrom().

Attack Path

  1. Compromise a renderer An attacker with a compromised or malicious renderer process (the standard SiteIsolation threat model) controls the contents of DidCommitProvisionalLoadParams, including params->origin.
  2. Trigger a synchronous about:blank commit The renderer creates a frame that loads about:blank synchronously (e.g. a sandboxed iframe with src="about:blank"), which commits without a browser-side NavigationRequest, exercising the vulnerable branch.
  3. Report a forged opaque origin Because the old check skipped validation for opaque origins, the renderer reports an arbitrary opaque origin (any precursor, or none); the browser accepts it and lets it replace the browser-derived last_committed_origin_ (demonstrated by the SyncAboutBlankCommitOriginReplacer test, which the post-patch code now rejects with RFHI_SYNCHRONOUS_COMMIT_ORIGIN_MISMATCH).
  4. Obtain an incorrect authorization state With the frame’s committed origin now under renderer control, subsequent origin-based authorization decisions in the browser can be made against an attacker-chosen origin (incorrect authorization in SiteIsolation) (inferred).

Impact Assessment

An attacker running a compromised renderer could cause the browser to record an attacker-controlled origin for a frame’s synchronous about:blank commit, defeating SiteIsolation’s origin-based authorization; the advisory classifies this as “Incorrect authorization in SiteIsolation” (AuthorizationError). The precondition is renderer control over the commit params plus the ability to trigger a synchronous about:blank commit. The vendor advisory rates this low severity and does not report in-the-wild exploitation; NVD scores it CVSS 6.5. Any further escalation (cross-origin data access, sandbox bypass) beyond the origin confusion is (inferred).

Changed Functions

FunctionChangeNotes
if
content/browser/renderer_host/render_frame_host_impl.cc
modified
SyncAboutBlankCommitOriginReplacer
content/browser/security_exploit_browsertest.cc
modified
IN_PROC_BROWSER_TEST_F
content/browser/security_exploit_browsertest.cc
modified

Files Changed

  • content/browser/bad_message.h
  • tools/metrics/histograms/metadata/stability/enums.xml
  • content/browser/renderer_host/render_frame_host_impl.cc
  • content/browser/security_exploit_browsertest.cc

Audit Directions

  • Opaque-origin short-circuits in commit validation
    Look for origin/URL consistency checks that special-case opaque() by skipping validation entirely; an opaque origin still carries a precursor that must be compared, not trusted blindly.
  • Invalid GURL into origin helpers
    Audit callers that pass Origin::GetURL() (empty for opaque origins) into functions like CanBeDerivedFrom() that DCHECK on invalid URLs, especially on paths reachable from renderer-supplied data.
  • Synchronous / NavigationRequest-less commit paths
    Review all DidCommitProvisionalLoadParams handling where navigation_request is null (synchronous about:blank, initial empty document), since browser-side invariants normally enforced via the NavigationRequest must be re-derived and validated from browser state instead.
From 2ddf8c2004f3fe43573fb900ce5cccd2fc660c52 Mon Sep 17 00:00:00 2001
From: Rakina Zata Amni <rakina@chromium.org>
Date: Mon, 14 Sep 2026 17:37:30 -0700
Subject: [PATCH] Validate opaque origins on synchronous about:blank commits

Before the first navigation in a frame, the browser sets the last
committed origin in SetOriginDependentStateOfNewFrame(). For synchronous
about:blank commits, the origin reported by the renderer in
DidCommitProvisionalLoadParams should agree with this browser-derived
value.

The existing check in DidCommitNavigationInternal() only covered the
case where the renderer reports a non-opaque origin, and when the
browser-derived origin was opaque it passed an empty GURL to
Origin::CanBeDerivedFrom(), which DCHECKs on invalid URLs. It also
accepted any opaque origin from the renderer, allowing the
browser-derived origin to be silently replaced.

This change branches on whether the browser-derived origin is opaque.
When it is, the renderer-reported origin must also be opaque with a
matching precursor; the nonce is not yet compared because the browser
and renderer can still derive different nonces for popups opened from
CSP-sandboxed documents with allow-popups-to-escape-sandbox. When it is
not opaque, the existing CanBeDerivedFrom() check is applied (now always
with a valid URL), and the "became opaque" case remains allowed for the
same popup scenario.

TAG=agy
CONV=5c1f3f18-89f3-4682-8400-f86fd31c0142

Bug: 498793976
Change-Id: I8db9b9f842660126984682f6340b417741e50e44
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8181529
Reviewed-by: Will Harris <wfh@chromium.org>
Reviewed-by: Charlie Reis <creis@chromium.org>
Commit-Queue: Rakina Zata Amni <rakina@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1697688}
---
diff --git a/content/browser/bad_message.h b/content/browser/bad_message.h
index af993c2..568abf9 100644
--- a/content/browser/bad_message.h
+++ b/content/browser/bad_message.h
@@ -374,7 +374,7 @@
   RFHI_WEBMCP_DUPLICATE_BIND = 346,
   RFHI_WEBMCP_DUPLICATE_SET_RECEIVER = 347,
   RFHI_WEBMCP_EXPOSED_UNTRUSTWORTHY_ORIGIN = 348,
-  RFHI_SYNCHONOUS_COMMIT_ORIGIN_MISMATCH = 349,
+  RFHI_SYNCHRONOUS_COMMIT_ORIGIN_MISMATCH = 349,
   RFHI_WEBMCP_INVALID_TOOL_OWNER = 350,
   NR_BAD_ORIGIN_HEADER = 351,
   RFH_DID_COMMIT_NAVIGATION_WHILE_BFCACHED = 352,
diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc
index 13a2167..d63fe6f3 100644
--- a/content/browser/renderer_host/render_frame_host_impl.cc
+++ b/content/browser/renderer_host/render_frame_host_impl.cc
@@ -16298,8 +16298,16 @@
       navigation_request
           ? navigation_request->GetUrlInfo().embedder_isolation_info
           : EmbedderIsolationInfo::CreateNone();
+  // Use `is_sandboxed` from `navigation_request` (if provided) to represent the
+  // newly committing document's sandbox state (which can differ from this
+  // RFH's active document sandbox state prior to commit, e.g. due to CSP
+  // sandbox response headers). When there is no `navigation_request` (e.g.,
+  // synchronous about:blank commits), fall back to the frame's active sandbox
+  // flags.
   bool is_sandboxed =
-      navigation_request && navigation_request->GetUrlInfo().is_sandboxed;
+      navigation_request
+          ? navigation_request->GetUrlInfo().is_sandboxed
+          : IsSandboxed(network::mojom::WebSandboxFlags::kOrigin);
 
   // Attempts to commit certain off-limits URL should be caught more strictly
   // than our FilterURL checks.  If a renderer violates this policy, it
@@ -16686,12 +16694,38 @@
     if (is_synchronous_about_blank_commit &&
         params->origin.scheme() != url::kFileScheme &&
         (!frame_tree_->is_guest() ||
-         !base::FeatureList::IsEnabled(features::kGuestViewMPArch)) &&
-        !params->origin.opaque() &&
-        !params->origin.CanBeDerivedFrom(last_committed_origin_.GetURL())) {
-      bad_message::ReceivedBadMessage(
-          GetProcess(), bad_message::RFHI_SYNCHONOUS_COMMIT_ORIGIN_MISMATCH);
-      return false;
+         !base::FeatureList::IsEnabled(features::kGuestViewMPArch))) {
+      bool origin_matches = false;
+      if (last_committed_origin_.opaque()) {
+        // When the browser-derived origin is opaque, the renderer-reported
+        // origin must also be opaque with a matching precursor.
+        // TODO(crbug.com/486082219): Compare origin nonces as well once nonces
+        // are synchronized for popups opened from CSP-sandboxed documents with
+        // allow-popups-to-escape-sandbox.
+        origin_matches =
+            params->origin.opaque() &&
+            params->origin.GetTupleOrPrecursorTupleIfOpaque() ==
+                last_committed_origin_.GetTupleOrPrecursorTupleIfOpaque();
+      } else if (params->origin.opaque()) {
+        // The browser-derived origin is not opaque but the renderer-reported
+        // one is. This can legitimately (but incorrectly) happen for popups
+        // opened from CSP-sandboxed documents with
+        // allow-popups-to-escape-sandbox, where the browser sees no sandbox
+        // flags on the frame but the renderer still inherits CSP sandbox flags
+        // for the document.
+        // TODO(crbug.com/486082219): Remove this handling once the incorrect
+        // case above can no longer happen and browser & renderer origins are
+        // always consistent.
+        origin_matches = true;
+      } else {
+        origin_matches =
+            params->origin.CanBeDerivedFrom(last_committed_origin_.GetURL());
+      }
+      if (!origin_matches) {
+        bad_message::ReceivedBadMessage(
+            GetProcess(), bad_message::RFHI_SYNCHRONOUS_COMMIT_ORIGIN_MISMATCH);
+        return false;
+      }
     }
 
     // Fill the redirect chain for the NavigationRequest. Since this is only for
diff --git a/tools/metrics/histograms/metadata/stability/enums.xml b/tools/metrics/histograms/metadata/stability/enums.xml
index f3ce4145..e954265b 100644
--- a/tools/metrics/histograms/metadata/stability/enums.xml
+++ b/tools/metrics/histograms/metadata/stability/enums.xml
@@ -524,7 +524,7 @@
   <int value="346" label="RFHI_WEBMCP_DUPLICATE_BIND"/>
   <int value="347" label="RFHI_WEBMCP_DUPLICATE_SET_RECEIVER"/>
   <int value="348" label="RFHI_WEBMCP_EXPOSED_UNTRUSTWORTHY_ORIGIN"/>
-  <int value="349" label="RFHI_SYNCHONOUS_COMMIT_ORIGIN_MISMATCH"/>
+  <int value="349" label="RFHI_SYNCHRONOUS_COMMIT_ORIGIN_MISMATCH"/>
   <int value="350" label="RFHI_WEBMCP_INVALID_TOOL_OWNER"/>
   <int value="351" label="NR_BAD_ORIGIN_HEADER"/>
   <int value="352" label="RFH_DID_COMMIT_NAVIGATION_WHILE_BFCACHED"/>
diff --git a/content/browser/security_exploit_browsertest.cc b/content/browser/security_exploit_browsertest.cc
index c029f4f..734f718 100644
--- a/content/browser/security_exploit_browsertest.cc
+++ b/content/browser/security_exploit_browsertest.cc
@@ -4828,6 +4828,40 @@
   url::Origin origin_override_;
 };
 
+// Interceptor that replaces the origin in DidCommitProvisionalLoadParams with
+// the specified value for synchronous about:blank commits, which arrive without
+// a NavigationRequest.
+class SyncAboutBlankCommitOriginReplacer
+    : public DidCommitNavigationInterceptor {
+ public:
+  SyncAboutBlankCommitOriginReplacer(WebContents* web_contents,
+                                     url::Origin origin_override)
+      : DidCommitNavigationInterceptor(web_contents),
+        origin_override_(std::move(origin_override)) {}
+
+  SyncAboutBlankCommitOriginReplacer(
+      const SyncAboutBlankCommitOriginReplacer&) = delete;
+  SyncAboutBlankCommitOriginReplacer& operator=(
+      const SyncAboutBlankCommitOriginReplacer&) = delete;
+
+  ~SyncAboutBlankCommitOriginReplacer() override = default;
+
+ protected:
+  bool WillProcessDidCommitNavigation(
+      RenderFrameHost* render_frame_host,
+      NavigationRequest* navigation_request,
+      mojom::DidCommitProvisionalLoadParamsPtr* params,
+      mojom::DidCommitProvisionalLoadInterfaceParamsPtr*) override {
+    if (!navigation_request && (*params)->url.IsAboutBlank()) {
+      (*params)->origin = origin_override_;
+    }
+    return true;
+  }
+
+ private:
+  url::Origin origin_override_;
+};
+
 }  // namespace
 
 IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, MismatchedOriginToCommit) {
@@ -4847,6 +4881,70 @@
   EXPECT_EQ(bad_message::RFH_ORIGIN_TO_COMMIT_MISMATCH, kill_waiter.Wait());
 }
 
+// Verify that a sandboxed iframe's synchronous about:blank commit rejects a
+// non-opaque origin when an opaque origin is expected.
+IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
+                       SandboxedIframeSyncCommitWithNonOpaqueOrigin) {
+  GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
+  EXPECT_TRUE(NavigateToURL(shell(), main_url));
+
+  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
+                            ->GetPrimaryFrameTree()
+                            .root();
+  url::Origin parent_origin =
+      root->current_frame_host()->GetLastCommittedOrigin();
+  EXPECT_FALSE(parent_origin.opaque());
+
+  // Set up an interceptor that modifies the about:blank commit's origin to
+  // test robustness against invalid origin reporting from the renderer.
+  SyncAboutBlankCommitOriginReplacer replacer(shell()->web_contents(),
+                                              parent_origin);
+  RenderProcessHostBadIpcMessageWaiter kill_waiter(
+      root->current_frame_host()->GetProcess());
+
+  // Create a sandboxed iframe with src="about:blank". The renderer performs a
+  // synchronous about:blank commit whose origin is modified above.
+  ExecuteScriptAsync(root,
+                     "var iframe = document.createElement('iframe');"
+                     "iframe.sandbox = '';"
+                     "iframe.src = 'about:blank';"
+                     "document.body.appendChild(iframe);");
+
+  EXPECT_EQ(bad_message::RFHI_SYNCHRONOUS_COMMIT_ORIGIN_MISMATCH,
+            kill_waiter.Wait());
+}
+
+// Verify that a sandboxed iframe's synchronous about:blank commit rejects an
+// opaque origin whose precursor does not match the expected precursor.
+IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
+                       SandboxedIframeSyncCommitWithMismatchedOpaqueOrigin) {
+  GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
+  EXPECT_TRUE(NavigateToURL(shell(), main_url));
+
+  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
+                            ->GetPrimaryFrameTree()
+                            .root();
+
+  // Set up an interceptor that modifies the about:blank commit's origin to an
+  // opaque origin with no precursor, testing error handling when precursors do
+  // not match.
+  SyncAboutBlankCommitOriginReplacer replacer(shell()->web_contents(),
+                                              url::Origin());
+  RenderProcessHostBadIpcMessageWaiter kill_waiter(
+      root->current_frame_host()->GetProcess());
+
+  // Create a sandboxed iframe with src="about:blank". The renderer performs a
+  // synchronous about:blank commit whose origin is modified above.
+  ExecuteScriptAsync(root,
+                     "var iframe = document.createElement('iframe');"
+                     "iframe.sandbox = '';"
+                     "iframe.src = 'about:blank';"
+                     "document.body.appendChild(iframe);");
+
+  EXPECT_EQ(bad_message::RFHI_SYNCHRONOUS_COMMIT_ORIGIN_MISMATCH,
+            kill_waiter.Wait());
+}
+
 // Ensure that the renderer is terminated if it sends an OpenURL IPC with an
 // invalid SWITCH_TO_TAB disposition.
 IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, OpenURLInvalidDisposition) {
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/security_exploit_browsertest.cc b/content/browser/security_exploit_browsertest.cc
index c029f4f..734f718 100644
--- a/content/browser/security_exploit_browsertest.cc
+++ b/content/browser/security_exploit_browsertest.cc
@@ -4828,6 +4828,40 @@
   url::Origin origin_override_;
 };
 
+// Interceptor that replaces the origin in DidCommitProvisionalLoadParams with
+// the specified value for synchronous about:blank commits, which arrive without
+// a NavigationRequest.
+class SyncAboutBlankCommitOriginReplacer
+    : public DidCommitNavigationInterceptor {
+ public:
+  SyncAboutBlankCommitOriginReplacer(WebContents* web_contents,
+                                     url::Origin origin_override)
+      : DidCommitNavigationInterceptor(web_contents),
+        origin_override_(std::move(origin_override)) {}
+
+  SyncAboutBlankCommitOriginReplacer(
+      const SyncAboutBlankCommitOriginReplacer&) = delete;
+  SyncAboutBlankCommitOriginReplacer& operator=(
+      const SyncAboutBlankCommitOriginReplacer&) = delete;
+
+  ~SyncAboutBlankCommitOriginReplacer() override = default;
+
+ protected:
+  bool WillProcessDidCommitNavigation(
+      RenderFrameHost* render_frame_host,
+      NavigationRequest* navigation_request,
+      mojom::DidCommitProvisionalLoadParamsPtr* params,
+      mojom::DidCommitProvisionalLoadInterfaceParamsPtr*) override {
+    if (!navigation_request && (*params)->url.IsAboutBlank()) {
+      (*params)->origin = origin_override_;
+    }
+    return true;
+  }
+
+ private:
+  url::Origin origin_override_;
+};
+
 }  // namespace
 
 IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, MismatchedOriginToCommit) {
@@ -4847,6 +4881,70 @@
   EXPECT_EQ(bad_message::RFH_ORIGIN_TO_COMMIT_MISMATCH, kill_waiter.Wait());
 }
 
+// Verify that a sandboxed iframe's synchronous about:blank commit rejects a
+// non-opaque origin when an opaque origin is expected.
+IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
+                       SandboxedIframeSyncCommitWithNonOpaqueOrigin) {
+  GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
+  EXPECT_TRUE(NavigateToURL(shell(), main_url));
+
+  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
+                            ->GetPrimaryFrameTree()
+                            .root();
+  url::Origin parent_origin =
+      root->current_frame_host()->GetLastCommittedOrigin();
+  EXPECT_FALSE(parent_origin.opaque());
+
+  // Set up an interceptor that modifies the about:blank commit's origin to
+  // test robustness against invalid origin reporting from the renderer.
+  SyncAboutBlankCommitOriginReplacer replacer(shell()->web_contents(),
+                                              parent_origin);
+  RenderProcessHostBadIpcMessageWaiter kill_waiter(
+      root->current_frame_host()->GetProcess());
+
+  // Create a sandboxed iframe with src="about:blank". The renderer performs a
+  // synchronous about:blank commit whose origin is modified above.
+  ExecuteScriptAsync(root,
+                     "var iframe = document.createElement('iframe');"
+                     "iframe.sandbox = '';"
+                     "iframe.src = 'about:blank';"
+                     "document.body.appendChild(iframe);");
+
+  EXPECT_EQ(bad_message::RFHI_SYNCHRONOUS_COMMIT_ORIGIN_MISMATCH,
+            kill_waiter.Wait());
+}
+
+// Verify that a sandboxed iframe's synchronous about:blank commit rejects an
+// opaque origin whose precursor does not match the expected precursor.
+IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
+                       SandboxedIframeSyncCommitWithMismatchedOpaqueOrigin) {
+  GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
+  EXPECT_TRUE(NavigateToURL(shell(), main_url));
+
+  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
+                            ->GetPrimaryFrameTree()
+                            .root();
+
+  // Set up an interceptor that modifies the about:blank commit's origin to an
+  // opaque origin with no precursor, testing error handling when precursors do
+  // not match.
+  SyncAboutBlankCommitOriginReplacer replacer(shell()->web_contents(),
+                                              url::Origin());
+  RenderProcessHostBadIpcMessageWaiter kill_waiter(
+      root->current_frame_host()->GetProcess());
+
+  // Create a sandboxed iframe with src="about:blank". The renderer performs a
+  // synchronous about:blank commit whose origin is modified above.
+  ExecuteScriptAsync(root,
+                     "var iframe = document.createElement('iframe');"
+                     "iframe.sandbox = '';"
+                     "iframe.src = 'about:blank';"
+                     "document.body.appendChild(iframe);");
+
+  EXPECT_EQ(bad_message::RFHI_SYNCHRONOUS_COMMIT_ORIGIN_MISMATCH,
+            kill_waiter.Wait());
+}
+
 // Ensure that the renderer is terminated if it sends an OpenURL IPC with an
 // invalid SWITCH_TO_TAB disposition.
 IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, OpenURLInvalidDisposition) {
Loading diff…