CVE-2026-8537
Overview
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
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.