Medium watch CVSS 6.5 chrome Info Disclosure 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactSide-channel information leakage in Navigation
DescriptionSide-channel information leakage in Navigation
ComponentNavigation
Bug ClassInfo Disclosure
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
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.
Tracker41485206
Fix commit9a4f5dc8779d (chromium/src) +36/-5
CWECWE-1300
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedLyra Rebane (rebane2001)
Reported2023-12-18 · bounty $2000 (per vendor advisory)
Fix landed2026-02-24 (public git)
Disclosed2026-04-07
Patch gap42 days — fix public in source before the advisory

Background

Same-document navigation
a navigation (e.g. a fragment/hash change) that updates history and URL within the existing document without a full document load.
Cross-origin owner `load` event
Blink fires load on an embedding iframe element (FrameOwner::DispatchLoad) for a same-document navigation initiated by a cross-origin frame, specifically so the cross-origin parent cannot distinguish same- vs. cross-document navigations.
`TaskHandle` / `PostDelayedCancellableTask`
Blink primitives for scheduling a cancellable delayed task; a stored TaskHandle lets later code cancel a still-pending task.
Navigation side channel (crbug.com/1248444)
a timing/observation technique by which a cross-origin parent infers information (e.g. the URL) of a cross-origin iframe from the pattern of observable load events.

Root Cause Analysis

When a same-document navigation in an iframe is initiated cross-origin and is cross-origin to its parent, the enclosing function in document_loader.cc (not named in the @@ -1822/@@ -1832 hunk headers) fires the owner iframe’s load event via frame_->Owner()->DispatchLoad() to mask whether the navigation was same- or cross-document. The pre-patch code dispatched this load event synchronously and once per navigation, so a cross-origin parent observing repeated same-document navigations saw a different count/cadence of load events than it would for repeated cross-document navigations (where a later navigation cancels the previous pending one), reconstituting the very side channel the masking was meant to close.

The fix coalesces these events: it stores a TaskHandle (cross_origin_parent_load_event_task_), cancels any still-pending one, and re-schedules DispatchLoad() on a 100 ms delayed cancellable task so rapid repeated same-document navigations collapse into roughly one load event, emulating cross-document cancellation behavior.

The patch also drops the now-redundant trailing guard frame_->Tree().Parent()->GetSecurityContext()->GetSecurityOrigin() from the condition, leaving the cross-origin !initiator_origin->CanAccess(...) check intact. The delayed task uses WrapWeakPersistent(frame_->Owner()) so it is safe if the owner goes away before it fires.

Key insight
The masking load event was fired synchronously and per-navigation, so its observable count still leaked same- vs. cross-document information across repeated navigations; the fix debounces it onto a cancellable 100 ms timer so repeated same-document navigations coalesce into a single load event that matches the cross-document case.

Attack Path

  1. Embed a cross-origin iframe An attacker page embeds a cross-origin iframe and arranges to initiate same-document (fragment) navigations in it that are cross-origin to the parent.
  2. Drive repeated same-document navigations The parent triggers a rapid series of fragment navigations on the iframe.
  3. Observe load-event cadence Pre-patch, each same-document navigation synchronously fired one owner load event, producing a count/timing pattern distinguishable from repeated cross-document navigations (which cancel each other).
  4. Infer cross-origin state The parent uses this observable load-event side channel to distinguish same- vs. cross-document navigation and thereby guess information such as the cross-origin iframe’s URL (crbug.com/1248444).

Impact Assessment

A malicious cross-origin parent page gains a side channel to infer navigation type (and thereby leak information such as a cross-origin iframe’s URL) from the pattern of owner load events, in the renderer/web-content context; the vendor advisory rates this medium. It is reachable by ordinary web content in a stock build with no special flag, extension, or policy, requiring only that the parent embed a cross-origin iframe and drive repeated same-document navigations; the diff addresses an information-leak timing channel and demonstrates no memory-safety impact.

Changed Functions

FunctionChangeNotes
BindOnce
third_party/blink/renderer/core/loader/document_loader.cc
modified

Files Changed

  • third_party/blink/renderer/core/loader/document_loader.cc
  • third_party/blink/renderer/core/loader/document_loader.h
  • third_party/blink/web_tests/http/tests/navigation/cross-origin-fragment-navigation-is-async-expected.txt
  • third_party/blink/web_tests/http/tests/navigation/cross-origin-fragment-navigation-is-async.html

Audit Directions

  • Masking events that remain countable
    Audit places that fire a “cover” event to hide cross-origin state and check whether its count, timing, or ordering still differs between the cases it is meant to make indistinguishable.
  • Sync vs. async cross-origin signals
    Review cross-origin-observable callbacks (load, error, message, resize) fired synchronously per operation that could be debounced/coalesced to match the baseline case.
  • Navigation-initiator origin checks
    Look at same-document navigation commit paths guarded by initiator_origin->CanAccess(...) for other observable differences (event order, promise resolution timing) between same- and cross-document navigations.
From 9a4f5dc8779d4e11b96c043fb54a2977b89664a1 Mon Sep 17 00:00:00 2001
From: Nate Chapin <japhet@chromium.org>
Date: Mon, 23 Feb 2026 17:08:34 -0800
Subject: [PATCH] Fire load event for same-document nav initiated by cross-origin parent on a delayed timer

Fixed: 41485206
Change-Id: I905e09a080d5bd715becf00c99375f3bc144240e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7560041
Reviewed-by: Charlie Reis <creis@chromium.org>
Commit-Queue: Nate Chapin <japhet@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1589055}
---

diff --git a/third_party/blink/renderer/core/loader/document_loader.cc b/third_party/blink/renderer/core/loader/document_loader.cc
index 17eeeb8..bcf06c7 100644
--- a/third_party/blink/renderer/core/loader/document_loader.cc
+++ b/third_party/blink/renderer/core/loader/document_loader.cc
@@ -454,6 +454,7 @@
       initial_permission_statuses;
   bool force_new_document_sequence_number;
   base::TimeDelta total_taken_time_to_update_subresource_load_metrics;
+  TaskHandle cross_origin_parent_load_event_task;
 };
 
 // Asserts size of DocumentLoader, so that whenever a new attribute is added to
@@ -1822,8 +1823,7 @@
 
   if (!frame_->GetDocument()->LoadEventStillNeeded() && frame_->Owner() &&
       initiator_origin &&
-      !initiator_origin->CanAccess(frame_->DomWindow()->GetSecurityOrigin()) &&
-      frame_->Tree().Parent()->GetSecurityContext()->GetSecurityOrigin()) {
+      !initiator_origin->CanAccess(frame_->DomWindow()->GetSecurityOrigin())) {
     // If this same-document navigation was initiated by a cross-origin iframe
     // and is cross-origin to its parent, fire onload on the owner iframe.
     // Normally, the owner iframe's onload fires if and only if the window's
@@ -1832,7 +1832,21 @@
     // load event to detect whether the navigation was same- or cross-document,
     // and can therefore try to guess the url of a cross-origin iframe. Fire the
     // iframe's onload to prevent this technique. https://crbug.com/1248444
-    frame_->Owner()->DispatchLoad();
+    // Fire the event on a delayed timer so that we only fire one load event for
+    // repeated same-document navigations. This allows us to fire roughly the
+    // same number of load events as if the navigation were cross-document in
+    // the repeated-navigation case, because each successive cross-document
+    // navigation would cancel the previous pending navigation.
+    if (cross_origin_parent_load_event_task_.IsActive()) {
+      cross_origin_parent_load_event_task_.Cancel();
+    }
+    constexpr static const base::TimeDelta cross_origin_load_event_delay =
+        base::Milliseconds(100);
+    cross_origin_parent_load_event_task_ = PostDelayedCancellableTask(
+        *frame_->GetTaskRunner(TaskType::kInternalLoading), FROM_HERE,
+        BindOnce([](FrameOwner* owner) { owner->DispatchLoad(); },
+                 WrapWeakPersistent(frame_->Owner())),
+        cross_origin_load_event_delay);
   }
 
   auto scroll_behavior = has_ua_visual_transition
diff --git a/third_party/blink/renderer/core/loader/document_loader.h b/third_party/blink/renderer/core/loader/document_loader.h
index 65e50de..113b7e93 100644
--- a/third_party/blink/renderer/core/loader/document_loader.h
+++ b/third_party/blink/renderer/core/loader/document_loader.h
@@ -881,6 +881,18 @@
   // Stores the total time taken by `UpdateSubresourceLoadMetrics()` for the
   // measurement purpose.
   base::TimeDelta total_taken_time_to_update_subresource_load_metrics_;
+
+  // Special case for same-document navigations initiated by a cross-origin
+  // frame: When a same-document navigation occurs in an iframe, we call
+  // FrameOwner::DispatchLoad() to fire a load event on the iframe that is
+  // embedding this frame. The parent frame containing that iframe might be
+  // cross-origin, and therefore shouldn't know whether the navigation was
+  // same-document or cross-document. We therefore schedule the DispatchLoad
+  // on a timer, which allows us to coalesce repeated same-document navigations
+  // into a single DispatchLoad, emulating the behavior of repeated
+  // cross-document navigations that will cancel each other if one doesn't have
+  // time to finish before the next one begins.
+  TaskHandle cross_origin_parent_load_event_task_;
 };
 
 DECLARE_WEAK_IDENTIFIER_MAP(DocumentLoader);
diff --git a/third_party/blink/web_tests/http/tests/navigation/cross-origin-fragment-navigation-is-async-expected.txt b/third_party/blink/web_tests/http/tests/navigation/cross-origin-fragment-navigation-is-async-expected.txt
index 09122d9c..cef31d4 100644
--- a/third_party/blink/web_tests/http/tests/navigation/cross-origin-fragment-navigation-is-async-expected.txt
+++ b/third_party/blink/web_tests/http/tests/navigation/cross-origin-fragment-navigation-is-async-expected.txt
@@ -1,6 +1,6 @@
 ALERT: iframe onload fired
 ALERT: PASS: url fragment is changing asynchronously
 ALERT: PASS: scheduled postMessage() before hashchange triggered.
-ALERT: iframe onload fired
 ALERT: PASS: hashchange triggered after postMessage().
+ALERT: iframe onload fired
 This tests that cross-origin-initiated fragment navigations are asynchronous and always fire the load event at their embedding iframe element if it's cross-origin. It does so by scheduling a postMessage before scheduling the navigation. If the navigation is synchronous, the internals API will be able to report the presence of an url fragment immediately.
diff --git a/third_party/blink/web_tests/http/tests/navigation/cross-origin-fragment-navigation-is-async.html b/third_party/blink/web_tests/http/tests/navigation/cross-origin-fragment-navigation-is-async.html
index a1fc3bc..b2cfc918 100644
--- a/third_party/blink/web_tests/http/tests/navigation/cross-origin-fragment-navigation-is-async.html
+++ b/third_party/blink/web_tests/http/tests/navigation/cross-origin-fragment-navigation-is-async.html
@@ -7,6 +7,8 @@
     testRunner.waitUntilDone();
 }
 
+window.iframe_onload_count = 0;
+
 window.onload = function() {
     window.postMessage("postmessage", "*");
     document.querySelector('iframe').src = "http://localhost:8000/navigation/resources/postmessage-on-hashchange.html#anchor1";
@@ -33,12 +35,15 @@
             alert('PASS: hashchange triggered after postMessage().');
         else
             alert('FAIL: hashchange triggered before postMessage().');
-        testRunner.notifyDone();
     }
 });
 
 function onloadFired() {
+    window.iframe_onload_count++;
     alert("iframe onload fired");
+    // 2 load events: 1 initial iframe load, 1 async fragment change.
+    if (window.iframe_onload_count == 2)
+        testRunner.notifyDone();
 }
 </script>
 <p>This tests that cross-origin-initiated fragment navigations are asynchronous
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/web_tests/http/tests/navigation/cross-origin-fragment-navigation-is-async-expected.txt b/third_party/blink/web_tests/http/tests/navigation/cross-origin-fragment-navigation-is-async-expected.txt
index 09122d9c..cef31d4 100644
--- a/third_party/blink/web_tests/http/tests/navigation/cross-origin-fragment-navigation-is-async-expected.txt
+++ b/third_party/blink/web_tests/http/tests/navigation/cross-origin-fragment-navigation-is-async-expected.txt
@@ -1,6 +1,6 @@
 ALERT: iframe onload fired
 ALERT: PASS: url fragment is changing asynchronously
 ALERT: PASS: scheduled postMessage() before hashchange triggered.
-ALERT: iframe onload fired
 ALERT: PASS: hashchange triggered after postMessage().
+ALERT: iframe onload fired
 This tests that cross-origin-initiated fragment navigations are asynchronous and always fire the load event at their embedding iframe element if it's cross-origin. It does so by scheduling a postMessage before scheduling the navigation. If the navigation is synchronous, the internals API will be able to report the presence of an url fragment immediately.
diff --git a/third_party/blink/web_tests/http/tests/navigation/cross-origin-fragment-navigation-is-async.html b/third_party/blink/web_tests/http/tests/navigation/cross-origin-fragment-navigation-is-async.html
index a1fc3bc..b2cfc918 100644
--- a/third_party/blink/web_tests/http/tests/navigation/cross-origin-fragment-navigation-is-async.html
+++ b/third_party/blink/web_tests/http/tests/navigation/cross-origin-fragment-navigation-is-async.html
@@ -7,6 +7,8 @@
     testRunner.waitUntilDone();
 }
 
+window.iframe_onload_count = 0;
+
 window.onload = function() {
     window.postMessage("postmessage", "*");
     document.querySelector('iframe').src = "http://localhost:8000/navigation/resources/postmessage-on-hashchange.html#anchor1";
@@ -33,12 +35,15 @@
             alert('PASS: hashchange triggered after postMessage().');
         else
             alert('FAIL: hashchange triggered before postMessage().');
-        testRunner.notifyDone();
     }
 });
 
 function onloadFired() {
+    window.iframe_onload_count++;
     alert("iframe onload fired");
+    // 2 load events: 1 initial iframe load, 1 async fragment change.
+    if (window.iframe_onload_count == 2)
+        testRunner.notifyDone();
 }
 </script>
 <p>This tests that cross-origin-initiated fragment navigations are asynchronous
Loading diff…

Original Bug Report

reported by re...@gmail.com

Security: Guess a cross-origin iframe URL by firing multiple navigations

VULNERABILITY DETAILS
This is a bypass for https://crbug.com/chromium/1248444.

It is possible to figure out whether a cross-origin iframe is on a specific URL by firing multiple hash-navigations at the same time, and then counting the onload events fired. If the URL you’re navigating to fires more than one load event, it was previously on the same URL.

Eg if the iframe is on example.org/foo and you navigate to example.org/foo#a, example.org/foo#b, and example.org/foo#c, you will get three onload events, but if the iframe starts off on example.org/bar you will only get one.

VERSION
Chrome Version: 122.0.6182.0 Dev + Stable
Operating System: Windows, Linux, macOS

REPRODUCTION CASE
In this example I am using www.chromium.org because it redirects to www.chromium.org/chromium-projects/, which we will detect.

  1. Download the poc.html file and open it.
  2. Write “https://www.chromium.org/" in both inputs.
  3. Click “Set URL” and wait for the page to load.
  4. Click “Check URL”. The URL in the iframe should NOT match.
  5. Repeat 2-4, but write “https://www.chromium.org/chromium-projects/" in the bottom input. This time the iframe URL should match.

CREDIT INFORMATION
Externally reported security bugs may appear in Chrome release notes. If
this bug is included, how would you like to be credited?
Reporter credit: Lyra Rebane (rebane2001)

View on issue tracker