High watch CVSS 4.3 chrome Policy Bypass 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient policy enforcement in ViewTransitions
DescriptionInsufficient policy enforcement in ViewTransitions
ComponentViewTransitions
Bug ClassPolicy Bypass
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.
Tracker495890000
Fix commite7b8d12a400f (chromium/src) +45/-5
CWECWE-942
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-03-24 (per vendor advisory)
Fix landed2026-03-26 (public git)
Disclosed2026-05-12
Patch gap47 days — fix public in source before the advisory

Background

Cross-document View Transitions
A feature that snapshots an outgoing document’s visual state and carries that ViewTransitionState into the newly committed document to animate between them.
`NavigationRequest`
The browser-side object tracking a navigation; it holds commit_params_->view_transition_state and view_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.

Key insight
The core mistake was trusting the origin captured at view-transition-state generation to still be valid at commit time, which prerender activation can violate; the fix re-checks the destination origin against the recorded view_transition_source_origin_ and discards the state on any mismatch.

Attack Path

  1. Generate transition state A document (origin A) initiates a cross-document view transition, so SetViewTransitionState stores the state/resources on the NavigationRequest along with (after the fix) origin A as the source origin.
  2. 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.
  3. State delivered cross-origin (pre-patch) Without the origin re-check, CommitPageActivation passes origin A’s view_transition_state/resources to origin B’s activated_entry/stored_page, initiating a transition across origins.
  4. 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

An attacker leveraging a prerender/activation origin change can cause view-transition state generated by one origin to be delivered to a document of a different origin, bypassing the same-origin restriction on view transitions in the browser process’s navigation path; the precondition is a cross-document view transition combined with an activation whose committed origin differs from the generating origin. The vendor advisory rates this high severity and describes it as insufficient policy enforcement in ViewTransitions. Concrete data exfiltration beyond the policy bypass itself is not demonstrated by the diff and is (inferred).

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

Audit Directions

  • Capture-time vs. commit-time origin
    Audit 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 swaps
    Review other NavigationRequest/StoredPage fields handed to a new document (resources, snapshots, tokens) to confirm each is re-checked against the committed origin.
  • Null-deref ordering in deferred conditions
    Look for guards (like !old_rfh_) placed after, rather than before, the first dereference of the guarded pointer in commit-deferring conditions.
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));
   }
 }
Loading diff…

Original Bug Report

reported by vi...@google.com

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