CVE-2026-79051
Overview
Background
- Text fragment (`#:~:text=`)
- a fragment directive that asks the browser to find, scroll to, and highlight matching text. It can also reveal
hidden=until-foundcontent, which firesbeforematch. Same-document text-fragment navigations are only honored when the browser started them or the initiator is same-origin (per the in-code comment and the WICG spec link). - `NavigateEvent.intercept()`
- the Navigation API lets a page’s
navigatehandler take over a navigation and turn it into a same-document commit. In Blink that commit goes throughDocumentLoader::RunURLAndHistoryUpdateStepswithSameDocumentNavigationType::kNavigationApiIntercept. - `NavigateEventDispatchParams`
- the struct carrying a navigation’s details (URL, destination item,
is_browser_initiated, and so on) from the commit path into theNavigateEvent. Before the patch it had no initiator-origin field. - `last_navigation_had_trusted_initiator_` / `has_text_fragment_token_`
DocumentLoaderstate. The first records whether the last navigation’s initiator was absent or same-origin. The second, computed byTextFragmentAnchor::GenerateNewTokenForSameDocument, decides whether a same-document text directive may be acted on.
Root Cause Analysis
When a same-document navigation was intercepted with NavigateEvent.intercept(), the commit was replayed through DocumentLoader::RunURLAndHistoryUpdateSteps. That function always passed the target frame’s own GetSecurityOrigin() as the initiator to UpdateForSameDocumentNavigation. Its removed comment explains the assumption: “callers of this method must already have performed same origin checks”. The original initiator, which could be a cross-origin iframe calling window.top.location.replace(), was therefore replaced by the target’s own origin, so the initiator-trust check on that path compared the target’s origin with itself and passed. Separately, TextFragmentAnchor::GenerateNewTokenForSameDocument returned false for any type other than kFragment. The DocumentLoader token-regeneration branch also ran only for kStandard or kFragment, so an intercepted kReplaceCurrentItem navigation did not recompute has_text_fragment_token_ at all.
The patch carries the initiator origin through NavigateEventDispatchParams::initiator_origin (set where the dispatch params are built) into RunURLAndHistoryUpdateSteps, which now prefers it over the frame’s origin. It also treats kNavigationApiIntercept like kFragment in both token checks. An intercepted cross-origin navigation now gets the same text-fragment decision as an un-intercepted one: blocked, as the browser test expects (did_match stays false, no scroll).
initiator_origin through the intercept commit and runs the text-fragment token logic for intercepted navigations.Attack Path
- Victim page intercepts navigations
the target page registers a
navigationnavigatelistener that callse.intercept()for interceptable navigations, a common single-page-app pattern. The browser test sets this up ona.test. - Cross-origin frame navigates the victim
an attacker-controlled cross-origin frame (an iframe from
b.testin the test) callswindow.top.location.replace()with the victim’s URL plus a#:~:text=directive. In the test this script runs withExecJs’s default user activation, and whether top-navigation restrictions allow it without activation is not shown by the patch. - Intercepted commit trusts the wrong origin
the navigation is intercepted and committed through
RunURLAndHistoryUpdateStepswith the victim frame’s own origin as initiator. The cross-origin initiator therefore looks same-origin, and the text-fragment token logic is skipped forkNavigationApiIntercept. - Text directive is processed
before the fix, the directive could be acted on (reveal of
hidden=until-foundcontent viabeforematch, scroll to the match), which the cross-origin restriction exists to prevent. - Cross-origin side channel an attacker who can observe whether a scroll or match happened could use this to test whether specific text exists in the victim page, an XS-Leak style oracle (inferred).
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
Elementthird_party/blink/renderer/core/navigation_api/navigate_event_dispatch_params.h |
modified | |
HistoryItemthird_party/blink/renderer/core/navigation_api/navigate_event_dispatch_params.h |
modified | |
SecurityOriginthird_party/blink/renderer/core/navigation_api/navigate_event_dispatch_params.h |
modified | |
SerializedScriptValuethird_party/blink/renderer/core/navigation_api/navigate_event_dispatch_params.h |
modified | |
ifcontent/browser/text_fragment_browsertest.cc |
modified | |
IN_PROC_BROWSER_TEST_Fcontent/browser/text_fragment_browsertest.cc |
modified |
Files Changed
third_party/blink/renderer/core/navigation_api/navigate_event.cccontent/browser/text_fragment_browsertest.ccthird_party/blink/renderer/core/fragment_directive/text_fragment_anchor.ccthird_party/blink/renderer/core/loader/document_loader.hthird_party/blink/renderer/core/loader/document_loader.ccthird_party/blink/renderer/core/navigation_api/navigate_event_dispatch_params.h
Audit Directions
- Initiator replaced by "self" on replay pathslook for other places where a navigation is deferred, intercepted, or replayed (Navigation API intercept,
history.pushState/replaceStatefunnels, prerender/BFCache activation,navigation.transitioncommits) and then defaults the initiator,is_browser_initiated, or user-activation state to the target frame’s own values. - Type allow-lists that miss new navigation typessearch for checks against
SameDocumentNavigationType::kFragment(and similar enums) that predatekNavigationApiIntercept. Each such check may silently skip a security decision for newer navigation kinds. - Initiator-gated featuresreview other features gated on “same-origin or browser-initiated” (scroll-to-text,
:targetbehaviors, autofocus, download/popup policies) for paths where the initiator origin is not threaded through to the decision point.
Patch
From c3c4adcd645122ce624b588e5f3609ae77b93f12 Mon Sep 17 00:00:00 2001 From: Keita Suzuki <[email protected]> Date: Sun, 05 Jul 2026 22:48:42 -0700 Subject: [PATCH] Preserve initiator origin for intercepted same-document commits When a same-document fragment navigation is intercepted via NavigateEvent.intercept(), the resulting commit goes through RunURLAndHistoryUpdateSteps() which previously always used the target frame's own origin as the initiator. Thread the original initiator origin from CommitSameDocumentNavigation() through NavigateEventDispatchParams so that the intercepted commit reflects who actually started the navigation. Also extend the same-document text-fragment token regeneration to cover kNavigationApiIntercept and teach GenerateNewTokenForSameDocument() to treat that type the same as a fragment navigation, so an intercepted navigation computes the same text-fragment token state as the equivalent un-intercepted one would. Add a content browsertest covering a cross-origin iframe that calls window.top.location.replace() with a text directive while the top frame intercepts the navigation. TAG=agy CONV=13ec7a8f-4d73-4c01-b1b7-4d465e984eb3 Bug: 513841856 Change-Id: I007fe1a7ac7132bd6b76348fe870ea416d33c238 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8017843 Reviewed-by: Takashi Toyoshima <[email protected]> Commit-Queue: Keita Suzuki <[email protected]> Reviewed-by: Rakina Zata Amni <[email protected]> Cr-Commit-Position: refs/heads/main@{#1656984} --- diff --git a/third_party/blink/renderer/core/fragment_directive/text_fragment_anchor.cc b/third_party/blink/renderer/core/fragment_directive/text_fragment_anchor.cc index 0d8f696..0fb4b09d 100644 --- a/third_party/blink/renderer/core/fragment_directive/text_fragment_anchor.cc +++ b/third_party/blink/renderer/core/fragment_directive/text_fragment_anchor.cc @@ -141,9 +141,12 @@ mojom::blink::SameDocumentNavigationType same_document_navigation_type) { if ((load_type != WebFrameLoadType::kStandard && load_type != WebFrameLoadType::kReplaceCurrentItem) || - same_document_navigation_type != - mojom::blink::SameDocumentNavigationType::kFragment) + (same_document_navigation_type != + mojom::blink::SameDocumentNavigationType::kFragment && + same_document_navigation_type != + mojom::blink::SameDocumentNavigationType::kNavigationApiIntercept)) { return false; + } // Same-document text fragment navigations are allowed only when initiated // from the browser process (e.g. typing in the omnibox) or a same-origin diff --git a/third_party/blink/renderer/core/loader/document_loader.cc b/third_party/blink/renderer/core/loader/document_loader.cc index 77bd92d..56f7d56 100644 --- a/third_party/blink/renderer/core/loader/document_loader.cc +++ b/third_party/blink/renderer/core/loader/document_loader.cc @@ -1028,15 +1028,20 @@ UserNavigationInvolvement involvement, PerformanceTimelineEntryIdInfo interaction_id, bool is_browser_initiated, - bool is_synchronously_committed) { - // We use the security origin of this frame since callers of this method must - // already have performed same origin checks. + bool is_synchronously_committed, + const SecurityOrigin* initiator_origin) { + // Unless the caller has explicitly provided an initiator (e.g. when + // committing an intercepted navigate event that was started by a different + // frame), use the security origin of this frame since callers of this + // method must already have performed same origin checks. // is_browser_initiated is false and is_synchronously_committed is true // because anything invoking this algorithm is a renderer-initiated navigation // in this process. UpdateForSameDocumentNavigation( new_url, history_item, same_document_navigation_type, std::move(data), - type, fire_popstate, frame_->DomWindow()->GetSecurityOrigin(), + type, fire_popstate, + initiator_origin ? initiator_origin + : frame_->DomWindow()->GetSecurityOrigin(), is_browser_initiated, is_synchronously_committed, LocalFrame::HasTransientUserActivation(frame_), involvement, /*has_ua_visual_transition*/ false, should_skip_screenshot, @@ -1102,7 +1107,12 @@ http_method_ = http_names::kGET; http_body_ = nullptr; } - + // Same-document text fragment navigations are restricted to same-origin or + // browser-initiated navigations for security. We must use the original + // initiator origin (which might be passed from an intercepted navigate event) + // rather than the target frame's origin. + // See + // https://wicg.github.io/scroll-to-text-fragment/#restricting-the-text-fragment last_navigation_had_trusted_initiator_ = !initiator_origin || (initiator_origin->IsSameOriginWith( frame_->DomWindow()->GetSecurityOrigin()) && @@ -1117,7 +1127,9 @@ // history API. if (type == WebFrameLoadType::kStandard || same_document_navigation_type == - mojom::blink::SameDocumentNavigationType::kFragment) { + mojom::blink::SameDocumentNavigationType::kFragment || + same_document_navigation_type == + mojom::blink::SameDocumentNavigationType::kNavigationApiIntercept) { has_text_fragment_token_ = TextFragmentAnchor::GenerateNewTokenForSameDocument( *this, type, same_document_navigation_type); @@ -1779,6 +1791,7 @@ params->involvement = involvement; params->source_element = source_element; params->destination_item = history_item; + params->initiator_origin = initiator_origin; params->is_browser_initiated = is_browser_initiated; params->has_ua_visual_transition = has_ua_visual_transition; params->is_synchronously_committed_same_document = diff --git a/third_party/blink/renderer/core/loader/document_loader.h b/third_party/blink/renderer/core/loader/document_loader.h index 3fd7501..e0a7887 100644 --- a/third_party/blink/renderer/core/loader/document_loader.h +++ b/third_party/blink/renderer/core/loader/document_loader.h @@ -253,7 +253,8 @@ PerformanceTimelineEntryIdInfo interaction_id = PerformanceTimelineEntryIdInfo::kNone, bool is_browser_initiated = false, - bool is_synchronously_committed = true); + bool is_synchronously_committed = true, + const SecurityOrigin* initiator_origin = nullptr); // |is_synchronously_committed| is described in comment for // CommitSameDocumentNavigation. diff --git a/third_party/blink/renderer/core/navigation_api/navigate_event.cc b/third_party/blink/renderer/core/navigation_api/navigate_event.cc index 97884cd..dc44705 100644 --- a/third_party/blink/renderer/core/navigation_api/navigate_event.cc +++ b/third_party/blink/renderer/core/navigation_api/navigate_event.cc @@ -450,13 +450,19 @@ // In our implementation, we call the corresponding function anyway, but // |type| being a reload type makes it do none of the spec-relevant // steps. Instead it does stuff like the loading spinner and use counters. + + // Pass the original initiator origin to ensure that security checks for the + // committed navigation (e.g., Scroll-to-Text Fragment restrictions) are + // evaluated against the actual initiator, even though the navigation was + // intercepted by this document. DomWindow()->document()->Loader()->RunURLAndHistoryUpdateSteps( dispatch_params_->url, dispatch_params_->destination_item, mojom::blink::SameDocumentNavigationType::kNavigationApiIntercept, state_object, ToWebFrameLoadType(navigation_type_), fire_popstate, dispatch_params_->should_skip_screenshot, dispatch_params_->involvement, dispatch_params_->interaction_id, dispatch_params_->is_browser_initiated, - dispatch_params_->is_synchronously_committed_same_document); + dispatch_params_->is_synchronously_committed_same_document, + dispatch_params_->initiator_origin.get()); if (LocalDOMWindow* window = DomWindow()) { window->GetFrame()->Loader().Progress().DidNavigationApiIntercept(); } diff --git a/third_party/blink/renderer/core/navigation_api/navigate_event_dispatch_params.h b/third_party/blink/renderer/core/navigation_api/navigate_event_dispatch_params.h index 4dd7efe..e014905f 100644 --- a/third_party/blink/renderer/core/navigation_api/navigate_event_dispatch_params.h +++ b/third_party/blink/renderer/core/navigation_api/navigate_event_dispatch_params.h @@ -20,6 +20,7 @@ class Element; class HistoryItem; +class SecurityOrigin; class SerializedScriptValue; enum class NavigateEventType { @@ -44,6 +45,7 @@ Member<Element> source_element; scoped_refptr<SerializedScriptValue> state_object; Member<HistoryItem> destination_item; + scoped_refptr<const SecurityOrigin> initiator_origin; bool is_browser_initiated = false; bool has_ua_visual_transition = false; bool is_synchronously_committed_same_document = true; diff --git a/content/browser/text_fragment_browsertest.cc b/content/browser/text_fragment_browsertest.cc index c5f91f11..066e390 100644 --- a/content/browser/text_fragment_browsertest.cc +++ b/content/browser/text_fragment_browsertest.cc @@ -640,6 +640,82 @@ } } +// Ensure same-document navigation to a text-fragment is blocked when initiated +// from a different origin and the destination intercepts the navigation via the +// navigation API. +IN_PROC_BROWSER_TEST_F( + TextFragmentAnchorBrowserTest, + SameDocumentScriptNavigationCrossOriginWithNavigationIntercept) { + ASSERT_TRUE(embedded_test_server()->Start()); + GURL url(embedded_test_server()->GetURL( + "a.test", "/scrollable_page_with_content.html")); + GURL target_text_url(embedded_test_server()->GetURL( + "a.test", "/scrollable_page_with_content.html#:~:text=hidden")); + GURL cross_origin_inner_url( + embedded_test_server()->GetURL("b.test", "/hello.html")); + + EXPECT_TRUE(NavigateToURL(shell(), url)); + + WebContentsImpl* main_contents = + static_cast<WebContentsImpl*>(shell()->web_contents()); + FrameTreeNode* root = main_contents->GetPrimaryFrameTree().root(); + + // Register a navigate handler that intercepts the same-document navigation + // and add a hidden=until-found target so we can detect whether the text + // directive was processed via the beforematch event. + EXPECT_TRUE(ExecJs(main_contents, + R"JS( + let target = document.createElement('div'); + target.hidden = 'until-found'; + target.textContent = 'hidden text'; + document.body.appendChild(target); + var did_match = false; + target.addEventListener('beforematch', () => { did_match = true; }); + navigation.addEventListener('navigate', e => { + if (e.canIntercept) { + e.intercept({handler: async () => {}}); + } + }); + )JS", + EXECUTE_SCRIPT_NO_USER_GESTURE)); + + // Insert a cross-origin iframe from which we'll execute script. + { + const auto script = JsReplace( + R"JS( + let f = document.createElement("iframe"); + f.src=$1; + document.body.appendChild(f); + )JS", + cross_origin_inner_url); + + TestNavigationObserver observer(main_contents); + EXPECT_TRUE(ExecJs(main_contents, script, EXECUTE_SCRIPT_NO_USER_GESTURE)); + observer.Wait(); + ASSERT_EQ(1u, root->child_count()); + } + + // Try navigating the top frame to a same-document text fragment from inside + // the iframe via location.replace(). The destination page intercepts the + // navigation, but the text directive should still be blocked because the + // initiator is cross-origin. + { + TestNavigationObserver observer(main_contents); + RenderFrameHostImpl* child_rfh = root->child_at(0)->current_frame_host(); + EXPECT_TRUE(ExecJs(child_rfh, JsReplace("window.top.location.replace($1);", + target_text_url))); + observer.Wait(); + EXPECT_EQ(target_text_url, main_contents->GetLastCommittedURL()); + + WaitForPageLoad(main_contents); + RunUntilInputProcessed(GetWidgetHost()); + RunUntilInputProcessed(GetWidgetHost()); + EXPECT_EQ(false, EvalJs(main_contents, "did_match;", + EXECUTE_SCRIPT_NO_USER_GESTURE)); + EXPECT_DID_SCROLL(false); + } +} + // Test that when ForceLoadAtTop document policy is explicitly turned off, // scrolling to a text fragment is allowed. IN_PROC_BROWSER_TEST_F(TextFragmentAnchorBrowserTest, EnabledByDocumentPolicy) {
Regression Test / PoC
diff --git a/content/browser/text_fragment_browsertest.cc b/content/browser/text_fragment_browsertest.cc
index c5f91f11..066e390 100644
--- a/content/browser/text_fragment_browsertest.cc
+++ b/content/browser/text_fragment_browsertest.cc
@@ -640,6 +640,82 @@
}
}
+// Ensure same-document navigation to a text-fragment is blocked when initiated
+// from a different origin and the destination intercepts the navigation via the
+// navigation API.
+IN_PROC_BROWSER_TEST_F(
+ TextFragmentAnchorBrowserTest,
+ SameDocumentScriptNavigationCrossOriginWithNavigationIntercept) {
+ ASSERT_TRUE(embedded_test_server()->Start());
+ GURL url(embedded_test_server()->GetURL(
+ "a.test", "/scrollable_page_with_content.html"));
+ GURL target_text_url(embedded_test_server()->GetURL(
+ "a.test", "/scrollable_page_with_content.html#:~:text=hidden"));
+ GURL cross_origin_inner_url(
+ embedded_test_server()->GetURL("b.test", "/hello.html"));
+
+ EXPECT_TRUE(NavigateToURL(shell(), url));
+
+ WebContentsImpl* main_contents =
+ static_cast<WebContentsImpl*>(shell()->web_contents());
+ FrameTreeNode* root = main_contents->GetPrimaryFrameTree().root();
+
+ // Register a navigate handler that intercepts the same-document navigation
+ // and add a hidden=until-found target so we can detect whether the text
+ // directive was processed via the beforematch event.
+ EXPECT_TRUE(ExecJs(main_contents,
+ R"JS(
+ let target = document.createElement('div');
+ target.hidden = 'until-found';
+ target.textContent = 'hidden text';
+ document.body.appendChild(target);
+ var did_match = false;
+ target.addEventListener('beforematch', () => { did_match = true; });
+ navigation.addEventListener('navigate', e => {
+ if (e.canIntercept) {
+ e.intercept({handler: async () => {}});
+ }
+ });
+ )JS",
+ EXECUTE_SCRIPT_NO_USER_GESTURE));
+
+ // Insert a cross-origin iframe from which we'll execute script.
+ {
+ const auto script = JsReplace(
+ R"JS(
+ let f = document.createElement("iframe");
+ f.src=$1;
+ document.body.appendChild(f);
+ )JS",
+ cross_origin_inner_url);
+
+ TestNavigationObserver observer(main_contents);
+ EXPECT_TRUE(ExecJs(main_contents, script, EXECUTE_SCRIPT_NO_USER_GESTURE));
+ observer.Wait();
+ ASSERT_EQ(1u, root->child_count());
+ }
+
+ // Try navigating the top frame to a same-document text fragment from inside
+ // the iframe via location.replace(). The destination page intercepts the
+ // navigation, but the text directive should still be blocked because the
+ // initiator is cross-origin.
+ {
+ TestNavigationObserver observer(main_contents);
+ RenderFrameHostImpl* child_rfh = root->child_at(0)->current_frame_host();
+ EXPECT_TRUE(ExecJs(child_rfh, JsReplace("window.top.location.replace($1);",
+ target_text_url)));
+ observer.Wait();
+ EXPECT_EQ(target_text_url, main_contents->GetLastCommittedURL());
+
+ WaitForPageLoad(main_contents);
+ RunUntilInputProcessed(GetWidgetHost());
+ RunUntilInputProcessed(GetWidgetHost());
+ EXPECT_EQ(false, EvalJs(main_contents, "did_match;",
+ EXECUTE_SCRIPT_NO_USER_GESTURE));
+ EXPECT_DID_SCROLL(false);
+ }
+}
+
// Test that when ForceLoadAtTop document policy is explicitly turned off,
// scrolling to a text fragment is allowed.
IN_PROC_BROWSER_TEST_F(TextFragmentAnchorBrowserTest, EnabledByDocumentPolicy) {
Original Bug Report
Navigation API intercept() bypass of TextFragment security via initiator laundering
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: The Navigation API’s intercept() method incorrectly ’launders’ a cross-origin initiator’s origin into a trusted same-origin context during same-document navigations. This allows an attacker to bypass Scroll-to-Text-Fragment security restrictions, potentially enabling cross-origin information leaks.
Affected files:
third_party/blink/renderer/core/loader/document_loader.ccthird_party/blink/renderer/core/navigation_api/navigate_event.ccthird_party/blink/renderer/core/fragment_directive/text_fragment_anchor.ccthird_party/blink/renderer/core/navigation_api/navigation_api.cc
Estimated timestamp from git blame: 2021-04-23
Summary
A potential vulnerability in Blink’s Navigation API allows a cross-origin attacker to bypass security restrictions for Scroll-to-Text-Fragment (STTF). By using navigation.intercept() during a same-document navigation (e.g., a hash change), the initiator of the navigation is incorrectly treated as same-origin. This ‘initiator laundering’ bypasses the security logic in TextFragmentAnchor, allowing an attacker to trigger text-fragment matching and its associated side effects (like expanding <details> elements) that would normally be blocked for cross-origin initiators.
Technical Details
The vulnerability arises from the interaction between the Navigation API and DocumentLoader during an intercepted same-document navigation:
- Initiator Laundering: When a navigation is intercepted via
event.intercept(), the commit process eventually callsDocumentLoader::RunURLAndHistoryUpdateSteps. This method invokesUpdateForSameDocumentNavigationbut hardcodes theinitiator_originas the frame’s own origin (frame_->DomWindow()->GetSecurityOrigin()), effectively overwriting the actual cross-origin initiator. (Seethird_party/blink/renderer/core/loader/document_loader.cc:1029). - Trusted Initiator State: As a result, the
last_navigation_had_trusted_initiator_flag inDocumentLoaderis set totrue. This flag is the primary signal used byTextFragmentAnchor::CheckSecurityRestrictionsto determine if cross-origin restrictions (e.g., blocking STTF in cross-origin iframes or popups) should be applied. - Token Persistence: For same-document navigations intercepted by the Navigation API,
DocumentLoader::UpdateForSameDocumentNavigationskips the logic that would normally regenerate or clear thehas_text_fragment_token_flag (seedocument_loader.cc:1108). This allows a token granted during an earlier trusted load (e.g., a user-activatedwindow.open()) to persist and be consumed by the attacker-controlled navigation. - Security Bypass: When the intercept handler resolves,
NavigateEvent::ProcessScrollBehaviorcreates aTextFragmentAnchor. The security checks inTextFragmentAnchor::CheckSecurityRestrictionspass because they find a valid token and a (laundered) trusted initiator.
Impact
While scrolling might be blocked, the side effects of text-fragment matching still occur. These include:
- Searching the victim’s DOM for attacker-specified text.
- Automatically expanding
<details>elements orhidden=until-foundregions containing the text. - Firing
beforematchevents.
An attacker can observe these side effects (e.g., through layout changes or timing attacks) to determine if specific text exists on a cross-origin page, leading to an XS-Leak (Cross-Site Leak).
Potential Reproduction Steps (Suggested)
- A victim page registers a navigation listener:
navigation.addEventListener('navigate', e => { if (e.canIntercept) e.intercept({handler: async () => {}}); });. - An attacker page opens the victim page in a popup with user activation:
const w = window.open('https://victim.example/');. This grants the initial text fragment token. - The attacker triggers a same-document navigation to a text fragment:
w.location.replace('https://victim.example/#:~:text=SECRET');. - The victim’s
navigateevent listener intercepts the navigation. - Due to the laundering bug, the STTF logic executes, revealing whether ‘SECRET’ exists on the page via observable side effects.
Suggested Fix
- Preserve Initiator: Modify
NavigateEventDispatchParamsto store and preserve the originalinitiator_originfrom the navigation request. - Update Commit Logic: Update
DocumentLoader::RunURLAndHistoryUpdateStepsto accept aninitiator_originparameter, and ensure the Navigation API passes the preserved origin during commit. - Manage Tokens: Ensure
UpdateForSameDocumentNavigationcorrectly clears or updateshas_text_fragment_token_for all same-document navigation types, including those intercepted by the Navigation API, if they are initiated from a cross-origin context.
Note: These findings and reproduction steps are based on a source code analysis; no automated proof of concept has been executed.
Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.