CVE-2026-8537
Overview
Background
- Cross-document View Transitions
- A feature that snapshots an outgoing document’s visual state and carries that
ViewTransitionStateinto the newly committed document to animate between them. - `NavigationRequest`
- The browser-side object tracking a navigation; it holds
commit_params_->view_transition_stateandview_transition_resources_to be handed to the destination document at commit. - Prerender / page activation
- A navigation can be served by activating a previously prepared
StoredPage/entry (CommitPageActivation), during which the destination origin may differ from the one that generated the view transition state. - Same-origin policy for VT
- Cross-origin view transitions are disallowed, because transition snapshots/state must not leak visual information across origins.
Root Cause Analysis
The view transition state and resources were captured from the old document and stored on the NavigationRequest, then at commit they were handed to activated_entry/stored_page without re-verifying that the destination origin still matched the origin that produced them. In a prerender activation, the destination origin can change between state capture and commit, so state generated by origin A could be delivered to a document of origin B, leaking view-transition state across origins.
The fix records the generating origin in a new view_transition_source_origin_ field (set in SetViewTransitionState from old_rfh_->GetLastCommittedOrigin()) and adds UpdateViewTransitionStateForDestinationOrigin, which clears both commit_params_->view_transition_state and view_transition_resources_ when the stored source origin is not same-origin with the actual GetOriginToCommit(). This check is invoked in CommitPageActivation before the state is passed to activated_entry/stored_page, so a mismatched-origin activation commits with no transition state. The deferring-condition change also reorders the if (!old_rfh_) return; guard ahead of the HasSubframeSnapshot() branch so old_rfh_ is confirmed non-null before its origin is read.
view_transition_source_origin_ and discards the state on any mismatch.Attack Path
- Generate transition state
A document (origin A) initiates a cross-document view transition, so
SetViewTransitionStatestores the state/resources on theNavigationRequestalong with (after the fix) origin A as the source origin. - Switch origin via prerender
The navigation is served through a prerender/page activation whose destination commits a different origin B, so
GetOriginToCommit()no longer matches the source origin. - State delivered cross-origin (pre-patch)
Without the origin re-check,
CommitPageActivationpasses origin A’sview_transition_state/resources to origin B’sactivated_entry/stored_page, initiating a transition across origins. - Cross-origin leak Origin B’s document receives view-transition state derived from origin A, leaking that visual/state information across the origin boundary (the exact observable leaked data is not enumerated by the diff and is (inferred)).
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/renderer_host/view_transition_commit_deferring_condition.cc |
modified |
Files Changed
content/browser/renderer_host/navigation_request.cccontent/browser/renderer_host/navigation_request.hcontent/browser/renderer_host/view_transition_commit_deferring_condition.cc
Audit Directions
- Capture-time vs. commit-time originAudit navigation state that is captured early (prerender, BFCache, speculative RFH) and consumed at commit for missing re-validation of the destination origin against the capturing origin.
- State carried across RFH swapsReview other
NavigationRequest/StoredPagefields handed to a new document (resources, snapshots, tokens) to confirm each is re-checked against the committed origin. - Null-deref ordering in deferred conditionsLook for guards (like
!old_rfh_) placed after, rather than before, the first dereference of the guarded pointer in commit-deferring conditions.
Patch
From e7b8d12a400f37ca6490cb24b8534ed7b5a00233 Mon Sep 17 00:00:00 2001
From: Vladimir Levin <vmpstr@chromium.org>
Date: Thu, 26 Mar 2026 11:46:35 -0700
Subject: [PATCH] cross-doc VT: double check the origin before sending view transitons
This patch ensures that we double check our origin before sending view
transition state to the new rfh. The origin may have switched in a
pre-render situation. See the referenced bug for details.
R=alexmos@chromium.org
Bug: 495890000
Change-Id: I7b94b5320858926dc914c9e7896856520a238e8a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7704912
Reviewed-by: Alex Moshchuk <alexmos@chromium.org>
Commit-Queue: Vladimir Levin <vmpstr@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1605690}
---
diff --git a/content/browser/renderer_host/navigation_request.cc b/content/browser/renderer_host/navigation_request.cc
index 41ee5180..c3ae8eb45 100644
--- a/content/browser/renderer_host/navigation_request.cc
+++ b/content/browser/renderer_host/navigation_request.cc
@@ -7064,6 +7064,18 @@
SendDeferredConsoleMessages();
}
+void NavigationRequest::UpdateViewTransitionStateForDestinationOrigin(
+ const url::Origin& origin) {
+ if (!commit_params().view_transition_state && !view_transition_resources_) {
+ return;
+ }
+ // Disallow cross origin view transitions.
+ if (!view_transition_source_origin_.IsSameOriginWith(origin)) {
+ commit_params_->view_transition_state.reset();
+ view_transition_resources_.reset();
+ }
+}
+
void NavigationRequest::CommitPageActivation() {
TRACE_EVENT("navigation", "NavigationRequest::CommitPageActivation",
perfetto::Flow::FromPointer(this));
@@ -7145,6 +7157,11 @@
// ReadyToCommitNavigation call).
page_activation_commit_time_ = base::TimeTicks::Now();
+ // Make sure to update the view transition state before passing the state to
+ // `activated_entry`. This may need to clear the state if the origin
+ // changed.
+ UpdateViewTransitionStateForDestinationOrigin(GetOriginToCommit().value());
+
// Use std::exchange instead of move, so that we clear out the optional on
// the commit_params.
activated_entry->SetViewTransitionState(
@@ -7192,6 +7209,11 @@
// ReadyToCommitNavigation call).
page_activation_commit_time_ = base::TimeTicks::Now();
+ // Make sure to update the view transition state before passing the state to
+ // `stored_page`. This may need to clear the state if the origin
+ // changed.
+ UpdateViewTransitionStateForDestinationOrigin(GetOriginToCommit().value());
+
// Use std::exchange instead of move, so that we clear out the optional on
// the commit_params.
stored_page->SetViewTransitionState(
@@ -11433,11 +11455,13 @@
#endif
void NavigationRequest::SetViewTransitionState(
+ const url::Origin& source_origin,
std::unique_ptr<ScopedViewTransitionResources> resources,
blink::ViewTransitionState view_transition_state) {
commit_params_->view_transition_state = std::move(view_transition_state);
CHECK(resources);
view_transition_resources_ = std::move(resources);
+ view_transition_source_origin_ = source_origin;
}
void NavigationRequest::ResetViewTransitionState() {
diff --git a/content/browser/renderer_host/navigation_request.h b/content/browser/renderer_host/navigation_request.h
index c63c187..028ebb3 100644
--- a/content/browser/renderer_host/navigation_request.h
+++ b/content/browser/renderer_host/navigation_request.h
@@ -1249,6 +1249,7 @@
// Initializes state which is passed from the old Document to the new Document
// for a ViewTransition.
void SetViewTransitionState(
+ const url::Origin& source_origin,
std::unique_ptr<ScopedViewTransitionResources> resources,
blink::ViewTransitionState view_transition_state);
@@ -2559,6 +2560,12 @@
// eventually be replaced with the navigation timeline metrics.
bool ShouldRecordNavigationTimelineUkm() const;
+ // Given the known destination origin, this updates the view transition state
+ // and resources. Namely, it clears it if the view transition state and
+ // resources were generated from a different origin with the given origin.
+ // This is because we disallow cross origin view transitions.
+ void UpdateViewTransitionStateForDestinationOrigin(const url::Origin& origin);
+
// Used for short-lived NavigationRequest created at DidCommit time for the
// purpose of committing navigation that were not driven by the browser
// process. This is used in only two cases:
@@ -3467,6 +3474,14 @@
// committing, the resources are destroyed with this request.
std::unique_ptr<ScopedViewTransitionResources> view_transition_resources_;
+ // An origin that generated the view transition state
+ // (`view_transition_resources_` and `commit_params_->view_transition_state`.
+ // This is used to ensure that at the time of commit, if the origin changed
+ // because this was a pre-render activation, we don't try and initiate a view
+ // transition since that can (unintentionally) leak view transition state
+ // across origins.
+ url::Origin view_transition_source_origin_;
+
// If true, this means that this navigation request was initiated by an
// animated transition.
bool was_initiated_by_animated_transition_ = false;
diff --git a/content/browser/renderer_host/view_transition_commit_deferring_condition.cc b/content/browser/renderer_host/view_transition_commit_deferring_condition.cc
index 05bd94f..c4ab3eb 100644
--- a/content/browser/renderer_host/view_transition_commit_deferring_condition.cc
+++ b/content/browser/renderer_host/view_transition_commit_deferring_condition.cc
@@ -203,11 +203,11 @@
base::ScopedClosureRunner runner(std::move(resume_navigation_));
- if (view_transition_state.HasSubframeSnapshot()) {
- if (!old_rfh_) {
- return;
- }
+ if (!old_rfh_) {
+ return;
+ }
+ if (view_transition_state.HasSubframeSnapshot()) {
// The subframe snapshot is only used for in-process iframes which don't own
// a widget.
if (old_rfh_->is_local_root()) {
@@ -231,7 +231,8 @@
resources_->set_delay_layer_tree_view_deletion(
view_transition_state.IsDelayLayerTreeViewDeletionEnabled());
NavigationRequest::From(&GetNavigationHandle())
- ->SetViewTransitionState(std::move(resources_),
+ ->SetViewTransitionState(old_rfh_->GetLastCommittedOrigin(),
+ std::move(resources_),
std::move(view_transition_state));
}
}
Original Bug Report
Potential cross-origin ViewTransition snapshot leak via same-site prerender redirect
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A potential vulnerability in Chrome’s ViewTransition mechanism may allow a same-site cross-origin renderer to receive pixel snapshots and DOM metadata from a victim page. This occurs because the same-origin security check during prerender activation relies on the initial requested URL rather than the actual committed origin of the redirected prerendered page. This could result in a cross-origin information leak if an attacker controls the redirection destination.
Affected files:
content/browser/renderer_host/view_transition_commit_deferring_condition.cccontent/browser/renderer_host/navigation_request.cccontent/browser/preloading/prerender/prerender_host.cc
Estimated timestamp from git blame: 2024-05-13
Summary
A potential vulnerability exists in the ViewTransition implementation where the same-origin security gate can be bypassed during prerender activation. This allows a same-site cross-origin page (e.g., sub2.example.com) to obtain pixel snapshots and DOM layout metadata from a victim page (e.g., sub1.example.com).
The core issue is that the check in ViewTransitionCommitDeferringCondition relies on NavigationRequest::GetTentativeOriginAtRequestTime(), which is derived from the navigation’s initial requested URL. If a prerendered page redirects to a same-site cross-origin destination (allowed via the Supports-Loading-Mode: credentialed-prerender header), the actual committed origin of the prerendered page diverges from the initial URL used for the same-origin gate.
Vulnerability Details
When a navigation involves a ViewTransition, the browser captures a snapshot of the current page to facilitate smooth animations. This snapshot includes GPU-side pixel references and sensitive DOM metadata such as bounding rects, CSS properties, and tag names. To prevent cross-origin information leaks, this state must only be delivered to a destination that is same-origin with the source.
During prerender activation:
ViewTransitionCommitDeferringCondition::MaybeCreateperforms a same-origin check by comparing theold_rfh->GetLastCommittedOrigin()againstnavigation_request->GetTentativeOriginAtRequestTime()whenis_running_potential_prerender_activation_checks()is true.GetTentativeOriginAtRequestTime()returns an origin based on the activation navigation’s requested URL (thecommon_params_->url).- In a scenario where a prerender for
sub1.example.com/redirectwas initiated, thePrerenderHostmatches based on this initial URL even if the navigation redirected to and committed atsub2.example.com(using thecredentialed-prerenderopt-in). - The ViewTransition gate compares the source origin (
sub1.example.com) against the tentative origin of the requested URL (sub1.example.com) and passes, allowing the snapshot to be captured. - After the gate passes,
NavigationRequest::OnPrerenderingActivationChecksCompleteproceeds with activation. - Later in the activation process (within
NavigationRequest::CommitPageActivation),common_params_->urlis updated to the prerendered page’s actual committed URL (rfh->GetLastCommittedURL()), but this occurs after the gate has already permitted the transition. - The
ViewTransitionStateis then transferred to the stored page and delivered to thesub2.example.comrenderer via theActivatePrerenderedPageMojo IPC without re-evaluating the origin.
Potential Attack Scenario
Note: These are suggested steps for a potential exploit path.
- Setup: An attacker controls
sub2.example.com; the victim controlssub1.example.com(both share the same eTLD+1). - Victim Configuration: The victim page at
sub1.example.comuses@view-transition { navigation: auto; }and speculation rules to prerender same-origin links. - Redirect: The victim site contains a URL
sub1.example.com/redirectthat HTTP-redirects tosub2.example.com/attacker-page. - Attacker Configuration:
sub2.example.com/attacker-pageresponds with the headerSupports-Loading-Mode: credentialed-prerenderto permit the same-site cross-origin prerender. - Trigger: A user on the victim site triggers a prerender of
sub1.example.com/redirect. The prerender redirects and commits at the attacker’s origin (sub2.example.com). - Activation: The user clicks the link to
sub1.example.com/redirect. The browser captures a snapshot of the victim’s page. Because the ViewTransition gate validates against the requested URL (sub1.example.com/redirect) rather than the actual committed origin (sub2.example.com), the check incorrectly passes. - Exfiltration: The attacker’s renderer receives the snapshot and DOM metadata, which can be accessed via CSS pseudo-elements (
::view-transition-old()) and JavaScript (getComputedStyle()).
Suggested Fix
During prerender activation, ViewTransitionCommitDeferringCondition::MaybeCreate should not rely solely on the initial tentative origin. Since the prerendered page has already committed in the background, the deferring condition should verify the same-origin requirement against the actual committed origin of the target RenderFrameHost associated with the matched PrerenderHost. Additionally or alternatively, NavigationRequest::CommitPageActivation should re-verify the origin after updating common_params_->url and clear the ViewTransitionState if it violates the same-origin policy.
Evaluated with Chrome root at commit: bb48272cafb7e24c93f55ef40da398cd206ee651
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. Please feel free to reach out to me if you have concerns or feedback.