CVE-2026-84355
Overview
Background
- Navigation API `navigate` event
- a same-origin-only event fired in the renderer so a document can observe/intercept its own navigations, including cross-document history traversals; it must not fire when the destination is cross-origin.
- `MaybeDispatchNavigateEventForCrossDocumentTraversal`
- the browser-side
NavigationRequestmethod that decides, at navigation start, whether the destination of a history traversal is same-origin with the current document and thus whether to dispatch thenavigateevent. - CSP sandbox / opaque origin
- a Content-Security-Policy
sandboxdirective can force a main-frame document to commit at an opaque (unique) origin, which is not same-origin with anything, even though its URL is unchanged. - `origin_related_state_` / `committed_origin`
- per-
FrameNavigationEntrystate onNavigationRequest; the patch addscommitted_origin, the origin actually recorded when that entry previously committed.
Root Cause Analysis
MaybeDispatchNavigateEventForCrossDocumentTraversal decided same-origin-ness by resolving the destination’s URL (url::Origin::Resolve(common_params_->url, initiator_origin)), which for a history entry that had committed at an opaque origin due to CSP sandbox does not reflect that opaque origin — the URL alone looks like an ordinary same-origin URL. As a result the check concluded “same-origin” and fired the navigate event for a cross-document history traversal to a CSP-sandboxed entry that was in fact cross-origin (opaque), violating the invariant that the event is same-origin-only.
The fix records the entry’s real committed_origin into origin_related_state_ when it is populated, and MaybeDispatchNavigateEventForCrossDocumentTraversal now prefers *origin_related_state_->committed_origin as destination_origin, falling back to the URL-derived origin only when no committed origin is available. A secondary correctness issue the reland fixes: when DeferSpeculativeRFHCreation is disabled, GetUrlInfo ran before response headers arrived and estimated sandbox flags only from commit_params_->frame_policy.sandbox_flags (empty for main-frame CSP sandbox), so UrlInfo looked unsandboxed, CanUseDestinationInstance() rejected the sandboxed destination SiteInstance, and the resulting ResetStateForSiteInstanceChange() wiped origin_related_state_ — erasing the very committed_origin the fix relies on; GetUrlInfo now also considers dest_site_instance_->GetSiteInfo().IsSandboxed() for history navigations so the estimate matches and the state is preserved.
navigate event fired cross-origin; the fix compares against the entry’s actual previously-committed_origin (and keeps the sandbox estimate consistent so that state is not reset away).Attack Path
- Create a CSP-sandboxed entry
A page navigates (in a frame it controls) to a document served with a CSP
sandboxdirective, which commits at an opaque origin while its URL remains an ordinary same-origin-looking URL, recorded as aFrameNavigationEntry. - Register a `navigate` listener
The current same-origin document installs a Navigation API
navigatehandler expecting to observe/intercept only same-origin navigations. - Traverse to the sandboxed entry
A cross-document history traversal (e.g.
history.back()/go) targets the sandboxed entry; pre-patch,MaybeDispatchNavigateEventForCrossDocumentTraversalresolves the destination from the pre-redirect URL, judges it same-origin, and dispatches thenavigateevent even though the true destination origin is opaque/cross-origin. - Cross-origin event exposure
The listener receives (and can intercept/inspect) a navigation to a cross-origin destination it should not have visibility into, an incorrect-authorization/information-exposure condition
(inferred)for the exact data leaked.
Impact Assessment
navigate event fire for a cross-document history traversal whose destination is actually a cross-origin opaque (CSP-sandboxed) origin, i.e. an event/observation that the same-origin policy should have withheld, in the renderer of the initiating document. Preconditions are a controllable CSP-sandboxed history entry and a same-origin navigate listener performing the traversal. No memory-safety impact or further escalation is demonstrated by the patch, and any beyond-spec leakage of destination details is (inferred).Files Changed
content/browser/renderer_host/navigation_controller_impl_browsertest.cccontent/browser/renderer_host/navigation_request.cccontent/browser/renderer_host/navigation_request.h
Audit Directions
- Origin decisions derived from URL instead of committed originSearch for same-origin/authorization checks that build an origin via
url::Origin::Resolve(url, ...)rather than the actually-committed origin; these mishandle opaque origins from CSP sandbox,data:, and sandboxed iframes. - Speculative estimates that diverge from final stateReview pre-response estimates (sandbox flags,
SiteInstancesuitability) that can trigger a state reset when they later mismatch, silently discarding fields other logic depends on. - Navigation API / cross-document traversal gatingAudit other
navigate-event and history-traversal same-origin gates for opaque-origin handling, especially where the destination origin is not yet known at navigation start.
Patch
From 35ec6fdc64cf53e460fcf54bb1aebe291889c53b Mon Sep 17 00:00:00 2001 From: Noam Rosenthal <[email protected]> Date: Thu, 03 Sep 2026 11:12:46 -0700 Subject: [PATCH] Reland "Use committed origin when deciding whether to dispatch navigate event" This is a reland of commit cc3f09b3a5179e1232fa27f898fa165cb06fc98b. Original change caused test failures in All/NavigationControllerBrowserTest.NavigateEventNotFiredForTraversalToCSPSandboxedEntry/* on builders where DeferSpeculativeRFHCreation is disabled or not applicable (e.g., fuchsia-arm64-cast-receiver-rel). Root cause: In DidCreateNavigationRequest, when DeferSpeculativeRFHCreation is disabled, RenderFrameHostManager::GetFrameHostForNavigation() runs synchronously at kBeforeNetworkRequest to create a speculative RFH. At this stage, HTTP response headers have not yet arrived. NavigationRequest::GetUrlInfo() only estimated sandbox flags from commit_params_->frame_policy.sandbox_flags, which is empty for main-frame CSP sandboxing. As a result, UrlInfo was estimated as unsandboxed, causing CanUseDestinationInstance() to conclude that the sandboxed destination SiteInstance was unsuitable. RenderFrameHostManager::GetSiteInstanceForNavigationRequest() observed this mismatch (request->dest_site_instance() != dest_site_instance) and called ResetStateForSiteInstanceChange(), which reset origin_related_state_. When MaybeDispatchNavigateEventForCrossDocumentTraversal() subsequently ran, origin_related_state_->committed_origin was missing, causing it to fall back to resolving the pre-redirect URL as same-origin and firing the navigate event unexpectedly. Fix: When computing UrlInfo before policies are computed, consider dest_site_instance_->GetSiteInfo().IsSandboxed() when estimating sandbox flags for history navigations. Original change description: > Use committed origin when deciding whether to dispatch navigate event > > The committed origin can be opaque in sandboxed iframes, and is also > pre-redirect. > > Bug: 511774376 > Change-Id: Ia5f4257a907274f24abb32186ae0e074eb488716 > Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8041464 > Reviewed-by: Charlie Reis <[email protected]> > Commit-Queue: Noam Rosenthal <[email protected]> > Reviewed-by: Nate Chapin <[email protected]> > Cr-Commit-Position: refs/heads/main@{#1662779} Bug: 511774376 Change-Id: Id05f189baabd0ef75e52dbf040ac5463b6d23cde Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8345238 Commit-Queue: Noam Rosenthal <[email protected]> Reviewed-by: Charlie Reis <[email protected]> Cr-Commit-Position: refs/heads/main@{#1691774} --- diff --git a/content/browser/renderer_host/navigation_request.cc b/content/browser/renderer_host/navigation_request.cc index 63b4aeb..b7e23d3 100644 --- a/content/browser/renderer_host/navigation_request.cc +++ b/content/browser/renderer_host/navigation_request.cc @@ -1889,6 +1889,7 @@ origin_related_state_.emplace(OriginRelatedState{ .item_sequence_number = frame_entry->item_sequence_number(), .document_sequence_number = frame_entry->document_sequence_number(), + .committed_origin = frame_entry->committed_origin(), }); } @@ -4759,10 +4760,12 @@ // // If PolicyContainer::ComputePoliciesToCommit() has run // `policy_container_builder_` will be valid, but even if it hasn't, we can - // speculatively take `commit_params_->frame_policy.sandbox_flags` if we - // haven't received the response yet and don't have the final - // `policy_container_builder_`, and if the state of the kOrigin flag changes, - // we'll detect the change and recompute the target SiteInstance elsewhere. + // speculatively take `commit_params_->frame_policy.sandbox_flags` (or + // `dest_site_instance_` for history navigations) if we haven't received the + // response yet and don't have the final `policy_container_builder_`, to + // reduce the chance that recomputing will be needed. If the state of the + // kOrigin flag changes, we'll detect the change and recompute the target + // SiteInstance elsewhere. // // In general, about:blank documents should stay in their initiator's process. // If neither the initiator or about:blank is sandboxed, or if both are, then @@ -4792,12 +4795,15 @@ // Note: We'll end up here if this function is called before // ComputePoliciesToCommit(), such as when computing a speculative // RenderFrameHost's SiteInstance before receiving a response. In that - // event we use the sandbox flags in commit_params_ as a current "best + // event we use the sandbox flags in commit_params_ (or the destination + // SiteInstance, if this is a history navigation) as a current "best // estimate". has_origin_restricted_sandbox_flag = - (commit_params_->frame_policy.sandbox_flags & - network::mojom::WebSandboxFlags::kOrigin) == - network::mojom::WebSandboxFlags::kOrigin; + ((commit_params_->frame_policy.sandbox_flags & + network::mojom::WebSandboxFlags::kOrigin) == + network::mojom::WebSandboxFlags::kOrigin) || + (dest_site_instance_ && + dest_site_instance_->GetSiteInfo().IsSandboxed()); } // It's possible that a sandbox attribute can disappear from a frame that @@ -12030,13 +12036,19 @@ blink::mojom::NavigationType::HISTORY_DIFFERENT_DOCUMENT) { return; } - // Only fire the navigate event if the destination is same-origin. Because - // this check is performed at navigation start time, `destination_origin` is + // Only fire the navigate event if the destination is same-origin. Prefer + // the origin recorded when the destination entry previously committed, since + // the URL alone does not reflect opaque origins resulting from CSP sandbox. + // See also PopulateSingleNavigationApiHistoryEntryVector(). Because this + // check is performed at navigation start time, the URL-derived fallback is // based on the pre-redirect URL, which is consistent with the renderer // process logic for firing the navigate event for non-history navigations. - url::Origin destination_origin = url::Origin::Resolve( - common_params_->url, - common_params_->initiator_origin.value_or(url::Origin())); + url::Origin destination_origin = + origin_related_state_ && origin_related_state_->committed_origin + ? *origin_related_state_->committed_origin + : url::Origin::Resolve( + common_params_->url, + common_params_->initiator_origin.value_or(url::Origin())); if (!frame_tree_node_->current_origin().IsSameOriginWith( destination_origin)) { return; diff --git a/content/browser/renderer_host/navigation_request.h b/content/browser/renderer_host/navigation_request.h index da65f8c..78a097fc 100644 --- a/content/browser/renderer_host/navigation_request.h +++ b/content/browser/renderer_host/navigation_request.h @@ -3209,6 +3209,9 @@ struct OriginRelatedState { int64_t item_sequence_number; int64_t document_sequence_number; + // The origin that was recorded when the FrameNavigationEntry was previously + // committed, if any. May be nullopt for entries that have never committed. + std::optional<url::Origin> committed_origin; }; std::optional<OriginRelatedState> origin_related_state_; diff --git a/content/browser/renderer_host/navigation_controller_impl_browsertest.cc b/content/browser/renderer_host/navigation_controller_impl_browsertest.cc index 7e83a45..65e762e 100644 --- a/content/browser/renderer_host/navigation_controller_impl_browsertest.cc +++ b/content/browser/renderer_host/navigation_controller_impl_browsertest.cc @@ -22020,6 +22020,10 @@ EXPECT_EQ(initial_site_instance, contents()->GetSiteInstance()); } +// TODO(crbug.com/511774376): Add a browser test to verify that the navigate +// event for a cross-document history traversal is not dispatched when the +// destination entry was committed at an opaque origin due to CSP sandbox. + IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest, NavigateToNavigationApiKey_NullCommittedOrigin) { // Ensure there's a history entry before the error page.
Regression Test / PoC
diff --git a/content/browser/renderer_host/navigation_controller_impl_browsertest.cc b/content/browser/renderer_host/navigation_controller_impl_browsertest.cc
index 7e83a45..65e762e 100644
--- a/content/browser/renderer_host/navigation_controller_impl_browsertest.cc
+++ b/content/browser/renderer_host/navigation_controller_impl_browsertest.cc
@@ -22020,6 +22020,10 @@
EXPECT_EQ(initial_site_instance, contents()->GetSiteInstance());
}
+// TODO(crbug.com/511774376): Add a browser test to verify that the navigate
+// event for a cross-document history traversal is not dispatched when the
+// destination entry was committed at an opaque origin due to CSP sandbox.
+
IN_PROC_BROWSER_TEST_P(NavigationControllerBrowserTest,
NavigateToNavigationApiKey_NullCommittedOrigin) {
// Ensure there's a history entry before the error page.