Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient policy enforcement in ViewTransitions
DescriptionInsufficient policy enforcement in ViewTransitions
ComponentViewTransitions
Bug ClassLogic Error
Tracker495890000
Fix commite7b8d12a400f (chromium/src) +45/-5
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-12

Changed Functions

FunctionChangeNotes
if
content/browser/renderer_host/view_transition_commit_deferring_condition.cc
modified

Files Changed

  • content/browser/renderer_host/navigation_request.cc
  • content/browser/renderer_host/navigation_request.h
  • content/browser/renderer_host/view_transition_commit_deferring_condition.cc
From e7b8d12a400f37ca6490cb24b8534ed7b5a00233 Mon Sep 17 00:00:00 2001
From: Vladimir Levin <[email protected]>
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.

[email protected]

Bug: 495890000
Change-Id: I7b94b5320858926dc914c9e7896856520a238e8a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7704912
Reviewed-by: Alex Moshchuk <[email protected]>
Commit-Queue: Vladimir Levin <[email protected]>
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));
   }
 }
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • content/browser/renderer_host/navigation_request.cc
  • content/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:

  1. ViewTransitionCommitDeferringCondition::MaybeCreate performs a same-origin check by comparing the old_rfh->GetLastCommittedOrigin() against navigation_request->GetTentativeOriginAtRequestTime() when is_running_potential_prerender_activation_checks() is true.
  2. GetTentativeOriginAtRequestTime() returns an origin based on the activation navigation’s requested URL (the common_params_->url).
  3. In a scenario where a prerender for sub1.example.com/redirect was initiated, the PrerenderHost matches based on this initial URL even if the navigation redirected to and committed at sub2.example.com (using the credentialed-prerender opt-in).
  4. 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.
  5. After the gate passes, NavigationRequest::OnPrerenderingActivationChecksComplete proceeds with activation.
  6. Later in the activation process (within NavigationRequest::CommitPageActivation), common_params_->url is updated to the prerendered page’s actual committed URL (rfh->GetLastCommittedURL()), but this occurs after the gate has already permitted the transition.
  7. The ViewTransitionState is then transferred to the stored page and delivered to the sub2.example.com renderer via the ActivatePrerenderedPage Mojo IPC without re-evaluating the origin.

Potential Attack Scenario

Note: These are suggested steps for a potential exploit path.

  1. Setup: An attacker controls sub2.example.com; the victim controls sub1.example.com (both share the same eTLD+1).
  2. Victim Configuration: The victim page at sub1.example.com uses @view-transition { navigation: auto; } and speculation rules to prerender same-origin links.
  3. Redirect: The victim site contains a URL sub1.example.com/redirect that HTTP-redirects to sub2.example.com/attacker-page.
  4. Attacker Configuration: sub2.example.com/attacker-page responds with the header Supports-Loading-Mode: credentialed-prerender to permit the same-site cross-origin prerender.
  5. 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).
  6. 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.
  7. 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.

View on issue tracker