CVE-2026-5876
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
BindOncethird_party/blink/renderer/core/loader/document_loader.cc |
modified |
Files Changed
third_party/blink/renderer/core/loader/document_loader.ccthird_party/blink/renderer/core/loader/document_loader.hthird_party/blink/web_tests/http/tests/navigation/cross-origin-fragment-navigation-is-async-expected.txtthird_party/blink/web_tests/http/tests/navigation/cross-origin-fragment-navigation-is-async.html
Patch
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
Regression Test / PoC
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
Original Bug Report
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.
- Download the
poc.htmlfile and open it. - Write “https://www.chromium.org/" in both inputs.
- Click “Set URL” and wait for the page to load.
- Click “Check URL”. The URL in the iframe should NOT match.
- 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)