CVE-2026-13790
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/core/fragment_directive/text_fragment_anchor_test.cc |
modified | |
TextFragmentAnchorTestthird_party/blink/renderer/core/fragment_directive/text_fragment_anchor_test.cc |
modified | |
TEST_Fthird_party/blink/renderer/core/fragment_directive/text_fragment_anchor_test.cc |
modified |
Files Changed
content/browser/renderer_host/render_frame_host_impl.ccthird_party/blink/public/mojom/frame/frame.mojomthird_party/blink/renderer/core/fragment_directive/text_fragment_anchor.ccthird_party/blink/renderer/core/fragment_directive/text_fragment_anchor_metrics_test.ccthird_party/blink/renderer/core/fragment_directive/text_fragment_anchor_test.cc
Patch
From edf09a64a6e305874741d429931de797eb63c1f4 Mon Sep 17 00:00:00 2001
From: Vladimir Levin <vmpstr@chromium.org>
Date: Thu, 07 May 2026 12:35:15 -0700
Subject: [PATCH] Reland "STTF: Delay sttf until related pages are finalized."
Fixed the test.
Original reason for revert: TextFragmentAnchorTest.AvoidScrollingIfHasOtherRelatedPages failed at https://ci.chromium.org/ui/p/chromium/builders/ci/Linux%20UBSan%20Tests/15610/overview
Original change's description:
> STTF: Delay sttf until related pages are finalized.
>
> This patch delays sttf until related pages are finalized. It also
> sends the browser-authoritative bool indicating whether there are
> related pages associated with this renderer.
>
> R=rakina@chromium.org
>
> Change-Id: Ifc03488451bd3d1e99c4ab447362437778ddf96d
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7629937
> Reviewed-by: Rakina Zata Amni <rakina@chromium.org>
> Commit-Queue: Vladimir Levin <vmpstr@chromium.org>
> Reviewed-by: Ken Buchanan <kenrb@chromium.org>
> Reviewed-by: Ari Chivukula <arichiv@chromium.org>
> Cr-Commit-Position: refs/heads/main@{#1625391}
Bug: 457771782
Change-Id: I9a36bfc18d12af87e96880bd565e670a2c9068ff
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7818478
Commit-Queue: Vladimir Levin <vmpstr@chromium.org>
Reviewed-by: Joe Mason <joenotcharles@google.com>
Reviewed-by: Rakina Zata Amni <rakina@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1627144}
---
diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc
index f3f4fbf..88879ec 100644
--- a/content/browser/renderer_host/render_frame_host_impl.cc
+++ b/content/browser/renderer_host/render_frame_host_impl.cc
@@ -16244,6 +16244,19 @@
IsInPrimaryMainFrame(), is_same_document_navigation,
navigation_ukm_builder);
+ if (is_main_frame() && !is_same_document_navigation) {
+ // The previous document is already pending delete and can't change the
+ // related pages list. However, the new document may not know about all of
+ // the related pages that were created from the previous document, see
+ // crbug.com/457771782. As a result, we let the renderer know that there are
+ // other related pages.
+ bool has_other_related_pages =
+ GetSiteInstance() &&
+ GetSiteInstance()->GetRelatedActiveContentsCount() > 1;
+ GetAssociatedLocalMainFrame()->NotifyRelatedPagesFinalized(
+ has_other_related_pages);
+ }
+
return true;
}
diff --git a/third_party/blink/public/mojom/frame/frame.mojom b/third_party/blink/public/mojom/frame/frame.mojom
index 3836008..913d342 100644
--- a/third_party/blink/public/mojom/frame/frame.mojom
+++ b/third_party/blink/public/mojom/frame/frame.mojom
@@ -1323,6 +1323,10 @@
FinalizeNavigationConfidence(
double randomized_trigger_rate,
ConfidenceLevel confidence);
+
+ // Called when the related pages set is finalized and cannot change due to
+ // previous page's mutations. Called when this (new page) is being committed.
+ NotifyRelatedPagesFinalized(bool has_other_related_pages);
};
// Implemented in Browser, this interface defines local-main-frame-specific
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 0f5ec51..0d8f696 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
@@ -83,7 +83,8 @@
return false;
}
- if (frame.GetPage()->RelatedPages().size()) {
+ if (frame.GetPage()->RelatedPages().size() ||
+ frame.GetPage()->HasOtherRelatedPagesDuringCommit()) {
TRACE_EVENT_INSTANT("blink", "CheckSecurityRestrictions", "Result",
"Non-Empty Browsing Context Group");
return false;
diff --git a/third_party/blink/renderer/core/fragment_directive/text_fragment_anchor_metrics_test.cc b/third_party/blink/renderer/core/fragment_directive/text_fragment_anchor_metrics_test.cc
index cf2e513..68c35d94 100644
--- a/third_party/blink/renderer/core/fragment_directive/text_fragment_anchor_metrics_test.cc
+++ b/third_party/blink/renderer/core/fragment_directive/text_fragment_anchor_metrics_test.cc
@@ -53,6 +53,12 @@
return scoped_fake_ukm_recorder_.recorder();
}
+ void ResetRelatedPagesFinalized() {
+ GetDocument()
+ .GetPage()
+ ->related_pages_mutation_from_previous_page_finalized_ = false;
+ }
+
base::HistogramTester histogram_tester_;
ScopedFakeUkmRecorder scoped_fake_ukm_recorder_;
};
@@ -250,6 +256,9 @@
<!DOCTYPE html>
<p>This is a test page</p>
)HTML");
+ GetDocument().GetPage()->NotifyRelatedPagesFinalized(false);
+ GetDocument().GetFrame()->Loader().ProcessPendingCrossDocumentFragment();
+ Compositor().BeginFrame();
Compositor().BeginFrame();
// The anchor should have been found and finalized.
@@ -370,6 +379,8 @@
<!DOCTYPE html>
<p id="element">This is a test page</p>
)HTML");
+ GetDocument().GetPage()->NotifyRelatedPagesFinalized(false);
+ GetDocument().GetFrame()->Loader().ProcessPendingCrossDocumentFragment();
FragmentAnchor* anchor =
GetDocument().GetFrame()->View()->GetFragmentAnchor();
if (anchor && anchor->IsTextFragmentAnchor()) {
@@ -668,6 +679,7 @@
}
{
+ ResetRelatedPagesFinalized();
SimRequest request("https://example.com/shadowtest.html#:~:text=ShadowDOM",
"text/html");
LoadURL("https://example.com/shadowtest.html#:~:text=ShadowDOM");
diff --git a/third_party/blink/renderer/core/fragment_directive/text_fragment_anchor_test.cc b/third_party/blink/renderer/core/fragment_directive/text_fragment_anchor_test.cc
index adce4c0..098e431 100644
--- a/third_party/blink/renderer/core/fragment_directive/text_fragment_anchor_test.cc
+++ b/third_party/blink/renderer/core/fragment_directive/text_fragment_anchor_test.cc
@@ -66,6 +66,11 @@
TextFragmentAnchorTestController() = default;
void BeginEmptyFrame() {
+ if (auto_finalize_) {
+ GetDocument().GetPage()->NotifyRelatedPagesFinalized(false);
+ GetDocument().GetFrame()->Loader().ProcessPendingCrossDocumentFragment();
+ }
+
// If a test case doesn't find a match and therefore doesn't schedule the
// beforematch event, we should still render a second frame as if we did
// schedule the event to retain test coverage.
@@ -76,6 +81,9 @@
Compositor().BeginFrame();
}
+ void SetAutoFinalize(bool auto_finalize) { auto_finalize_ = auto_finalize; }
+
+ public:
ScrollableArea* LayoutViewport() {
return GetDocument().View()->LayoutViewport();
}
@@ -170,6 +178,8 @@
"Implement others if new modality is needed.";
}
}
+
+ bool auto_finalize_ = true;
};
class TextFragmentAnchorTest : public TextFragmentAnchorTestController {
@@ -198,7 +208,7 @@
)HTML");
RunAsyncMatchingTasks();
- Compositor().BeginFrame();
+ BeginEmptyFrame();
Element& p = *GetDocument().getElementById(AtomicString("text"));
@@ -208,6 +218,109 @@
<< LayoutViewport()->GetScrollOffset().ToString();
}
+// Test that scroll to text fragment is deferred until related pages are
+// finalized.
+TEST_F(TextFragmentAnchorTest, DeferUntilRelatedPagesFinalized) {
+ SetAutoFinalize(false);
+ SimRequest::Params params;
+ params.requestor_origin =
+ WebSecurityOrigin::CreateFromString(WebString("https://example.org"));
+ SimRequest request("https://example.com/test.html#:~:text=test", "text/html",
+ params);
+ LoadURL("https://example.com/test.html#:~:text=test");
+
+ ASSERT_FALSE(
+ GetDocument().GetPage()->RelatedPagesMutationFromPreviousPageFinalized());
+
+ request.Complete(R"HTML(
+ <!DOCTYPE html>
+ <style>
+ body {
+ height: 1200px;
+ }
Regression Test / PoC
diff --git a/third_party/blink/renderer/core/fragment_directive/text_fragment_anchor_metrics_test.cc b/third_party/blink/renderer/core/fragment_directive/text_fragment_anchor_metrics_test.cc
index cf2e513..68c35d94 100644
--- a/third_party/blink/renderer/core/fragment_directive/text_fragment_anchor_metrics_test.cc
+++ b/third_party/blink/renderer/core/fragment_directive/text_fragment_anchor_metrics_test.cc
@@ -53,6 +53,12 @@
return scoped_fake_ukm_recorder_.recorder();
}
+ void ResetRelatedPagesFinalized() {
+ GetDocument()
+ .GetPage()
+ ->related_pages_mutation_from_previous_page_finalized_ = false;
+ }
+
base::HistogramTester histogram_tester_;
ScopedFakeUkmRecorder scoped_fake_ukm_recorder_;
};
@@ -250,6 +256,9 @@
<!DOCTYPE html>
<p>This is a test page</p>
)HTML");
+ GetDocument().GetPage()->NotifyRelatedPagesFinalized(false);
+ GetDocument().GetFrame()->Loader().ProcessPendingCrossDocumentFragment();
+ Compositor().BeginFrame();
Compositor().BeginFrame();
// The anchor should have been found and finalized.
@@ -370,6 +379,8 @@
<!DOCTYPE html>
<p id="element">This is a test page</p>
)HTML");
+ GetDocument().GetPage()->NotifyRelatedPagesFinalized(false);
+ GetDocument().GetFrame()->Loader().ProcessPendingCrossDocumentFragment();
FragmentAnchor* anchor =
GetDocument().GetFrame()->View()->GetFragmentAnchor();
if (anchor && anchor->IsTextFragmentAnchor()) {
@@ -668,6 +679,7 @@
}
{
+ ResetRelatedPagesFinalized();
SimRequest request("https://example.com/shadowtest.html#:~:text=ShadowDOM",
"text/html");
LoadURL("https://example.com/shadowtest.html#:~:text=ShadowDOM");
diff --git a/third_party/blink/renderer/core/fragment_directive/text_fragment_anchor_test.cc b/third_party/blink/renderer/core/fragment_directive/text_fragment_anchor_test.cc
index adce4c0..098e431 100644
--- a/third_party/blink/renderer/core/fragment_directive/text_fragment_anchor_test.cc
+++ b/third_party/blink/renderer/core/fragment_directive/text_fragment_anchor_test.cc
@@ -66,6 +66,11 @@
TextFragmentAnchorTestController() = default;
void BeginEmptyFrame() {
+ if (auto_finalize_) {
+ GetDocument().GetPage()->NotifyRelatedPagesFinalized(false);
+ GetDocument().GetFrame()->Loader().ProcessPendingCrossDocumentFragment();
+ }
+
// If a test case doesn't find a match and therefore doesn't schedule the
// beforematch event, we should still render a second frame as if we did
// schedule the event to retain test coverage.
@@ -76,6 +81,9 @@
Compositor().BeginFrame();
}
+ void SetAutoFinalize(bool auto_finalize) { auto_finalize_ = auto_finalize; }
+
+ public:
ScrollableArea* LayoutViewport() {
return GetDocument().View()->LayoutViewport();
}
@@ -170,6 +178,8 @@
"Implement others if new modality is needed.";
}
}
+
+ bool auto_finalize_ = true;
};
class TextFragmentAnchorTest : public TextFragmentAnchorTestController {
@@ -198,7 +208,7 @@
)HTML");
RunAsyncMatchingTasks();
- Compositor().BeginFrame();
+ BeginEmptyFrame();
Element& p = *GetDocument().getElementById(AtomicString("text"));
@@ -208,6 +218,109 @@
<< LayoutViewport()->GetScrollOffset().ToString();
}
+// Test that scroll to text fragment is deferred until related pages are
+// finalized.
+TEST_F(TextFragmentAnchorTest, DeferUntilRelatedPagesFinalized) {
+ SetAutoFinalize(false);
+ SimRequest::Params params;
+ params.requestor_origin =
+ WebSecurityOrigin::CreateFromString(WebString("https://example.org"));
+ SimRequest request("https://example.com/test.html#:~:text=test", "text/html",
+ params);
+ LoadURL("https://example.com/test.html#:~:text=test");
+
+ ASSERT_FALSE(
+ GetDocument().GetPage()->RelatedPagesMutationFromPreviousPageFinalized());
+
+ request.Complete(R"HTML(
+ <!DOCTYPE html>
+ <style>
+ body {
+ height: 1200px;
+ }
+ p {
+ position: absolute;
+ top: 1000px;
+ }
+ </style>
+ <p id="text">This is a test page</p>
+ )HTML");
+
+ ThreadScheduler::Current()
+ ->ToMainThreadScheduler()
+ ->StartIdlePeriodForTesting();
+ task_environment().FastForwardUntilNoTasksRemain();
+ BeginEmptyFrame();
+
+ ASSERT_FALSE(
+ GetDocument().GetPage()->RelatedPagesMutationFromPreviousPageFinalized());
+
+ Element& p = *GetDocument().getElementById(AtomicString("text"));
+
+ EXPECT_EQ(nullptr, GetDocument().CssTarget());
+ EXPECT_FALSE(ViewportRect().Contains(BoundingRectInFrame(p)));
+
+ GetDocument().GetPage()->NotifyRelatedPagesFinalized(
+ /* has_other_related_pages= */ false);
+ GetDocument().GetFrame()->Loader().ProcessPendingCrossDocumentFragment();
+
+ RunAsyncMatchingTasks();
+ BeginEmptyFrame();
+
+ EXPECT_EQ(p, *GetDocument().CssTarget());
+ EXPECT_TRUE(ViewportRect().Contains(BoundingRectInFrame(p)));
+}
+
+TEST_F(TextFragmentAnchorTest, AvoidScrollingIfHasOtherRelatedPages) {
+ SetAutoFinalize(false);
+ SimRequest::Params params;
+ params.requestor_origin =
+ WebSecurityOrigin::CreateFromString(WebString("https://example.org"));
+ SimRequest request("https://example.com/test.html#:~:text=test", "text/html",
+ params);
+ LoadURL("https://example.com/test.html#:~:text=test");
+
+ ASSERT_FALSE(
+ GetDocument().GetPage()->RelatedPagesMutationFromPreviousPageFinalized());
+
+ request.Complete(R"HTML(
+ <!DOCTYPE html>
+ <style>
+ body {
+ height: 1200px;
+ }
+ p {
+ position: absolute;
+ top: 1000px;
+ }
+ </style>
+ <p id="text">This is a test page</p>
+ )HTML");
+
+ ThreadScheduler::Current()
+ ->ToMainThreadScheduler()
+ ->StartIdlePeriodForTesting();
+ task_environment().FastForwardUntilNoTasksRemain();
+ BeginEmptyFrame();
+
+ ASSERT_FALSE(
+ GetDocument().GetPage()->RelatedPagesMutationFromPreviousPageFinalized());
+
+ Element& p = *GetDocument().getElementById(AtomicString("text"));
+
+ EXPECT_EQ(nullptr, GetDocument().CssTarget());
+ EXPECT_FALSE(ViewportRect().Contains(BoundingRectInFrame(p)));
+
+ GetDocument().GetPage()->NotifyRelatedPagesFinalized(
+ /* has_other_related_pages= */ true);
+ GetDocument().GetFrame()->Loader().ProcessPendingCrossDocumentFragment();
+
+ test::RunPendingTasks();
+
+ EXPECT_EQ(nullptr, GetDocument().CssTarget());
+ EXPECT_FALSE(ViewportRect().Contains(BoundingRectInFrame(p)));
+}
+
// Basic test case for silent scroll directives, ensure we scroll the matching
// text into view but do NOT apply :target or markers.
TEST_F(TextFragmentAnchorTest, BasicSilentScrollTest) {
@@ -388,7 +501,7 @@
)HTML");
RunAsyncMatchingTasks();
- Compositor().BeginFrame();
+ BeginEmptyFrame();
EXPECT_EQ(nullptr, GetDocument().CssTarget());
EXPECT_FALSE(GetDocument().View()->GetFragmentAnchor());
@@ -423,7 +536,7 @@
// Force a layout
GetDocument().body()->setAttribute(html_names::kStyleAttr,
AtomicString("height: 1300px"));
- Compositor().BeginFrame();
+ BeginEmptyFrame();
EXPECT_EQ(nullptr, GetDocument().CssTarget());
EXPECT_TRUE(GetDocument().Markers().Markers().empty());
@@ -453,7 +566,7 @@
)HTML");
RunAsyncMatchingTasks();
- Compositor().BeginFrame();
+ BeginEmptyFrame();
Element& first = *GetDocument().getElementById(AtomicString("first"));
@@ -488,7 +601,7 @@
)HTML");
RunAsyncMatchingTasks();
- Compositor().BeginFrame();
+ BeginEmptyFrame();
Element& match = *GetDocument().getElementById(AtomicString("match"));
@@ -524,7 +637,7 @@
)HTML");
RunAsyncMatchingTasks();
- Compositor().BeginFrame();
+ BeginEmptyFrame();
Element& first = *GetDocument().getElementById(AtomicString("first"));
@@ -561,7 +674,7 @@
)HTML");
RunAsyncMatchingTasks();
- Compositor().BeginFrame();
+ BeginEmptyFrame();
Element& second = *GetDocument().getElementById(AtomicString("second"));
@@ -594,7 +707,7 @@
)HTML");
RunAsyncMatchingTasks();
- Compositor().BeginFrame();
+ BeginEmptyFrame();
Element& p = *GetDocument().getElementById(AtomicString("text"));
@@ -637,7 +750,7 @@
// Force a layout
GetDocument().body()->setAttribute(html_names::kStyleAttr,
AtomicString("height: 1300px"));
- Compositor().BeginFrame();
+ BeginEmptyFrame();
EXPECT_EQ(nullptr, GetDocument().CssTarget());
EXPECT_TRUE(GetDocument().Markers().Markers().empty());
@@ -1147,6 +1260,9 @@
)HTML");
GetDocument().View()->UpdateAllLifecyclePhasesForTest();
+ GetDocument().GetPage()->NotifyRelatedPagesFinalized(false);
+ GetDocument().GetFrame()->Loader().ProcessPendingCrossDocumentFragment();
+
mojom::blink::ScrollType scroll_type = GetParam();
cc::ScrollSourceType source_type =
(scroll_type == mojom::blink::ScrollType::kAnchoring ||
@@ -1161,7 +1277,7 @@
img_request.Complete("");
RunAsyncMatchingTasks();
- Compositor().BeginFrame();
+ BeginEmptyFrame();
Element& p = *GetDocument().getElementById(AtomicString("text"));
@@ -1227,7 +1343,7 @@
LayoutViewport()->SetScrollOffset(ScrollOffset(0, -10), scroll_type,
source_type);
- Compositor().BeginFrame();
+ BeginEmptyFrame();
EXPECT_EQ(2u, GetDocument().Markers().Markers().size());
}
@@ -1256,7 +1372,7 @@
</p>
... (truncated)
Original Bug Report
STTF allows to leaking text char by char from cross-origin page.
Steps to reproduce the problem
Steps to reproduce:
- Download attached files
- Change
TARGET,PREFIX,FOUND,NOT_FOUNDif you want to test on another text/website. - Host this files on localhost/site
- open
exploit_chrome.html - Start typing text
You will see that symbols will gradually start appearing on the page.
See example in PoC.mov.
Problem Description
We have discovered that using STTF (Scroll To Text Fragment) makes it possible to detect and exfiltrate text from another page. This is possible due to several factors:
Bypass of Empty BCG check
Firstly, by default, Chromium does not allow the use of STTF if there are other windows in the current Browser Context Group and our page is cross-origin. This check can be found in CheckSecurityResrictions:
if (frame.GetPage()->RelatedPages().size()) {
TRACE_EVENT_INSTANT("blink", "CheckSecurityRestrictions", "Result",
"Non-Empty Browsing Context Group");
return false;
}
This check can be bypassed as follows:
window.location.href=`https://cross-origin-site/#:~:text=...`
window.open('/same-origin-with-attacker')
Based on my tests, this happens because the check of frame.GetPage()->RelatedPages().size() occurs during the redirect, and only after that does the new window begin to open.
Time difference
We discovered that when Chromium opens a link with an STTF, redirects may fail to execute if the text search is under heavy load. This happens because:
-
First, the text is searched using
void TextAnnotationSelector::FindRange, which performs a linear search for text. -
When the text is found (even partially),
bool IsWordBoundedand other checks are performed. -
Only after these steps does the highlight and scroll to text occur.
It can be observed that the number of instructions executed will differ, but for a simple search, the difference is negligible.
Therefore, we used another STTF feature to increase the number of instructions and thus slow down the search time — :~:text=...&text=...&text=...&text=.......
Since STTF allows us to insert an unlimited number of text fragments, we can add tens of thousands of identical text fragments in a single URL. This slows execution down, as the operation will be performed tens of thousands of times instead of just once.
Detect time difference by history length
There is no traditional way to detect STTF time. However, we have discovered a way to do this:
let w = window.opener
await sleep(TIME);
w.location = TARGET + "#1";
...
w.location = TARGET + "#5";
w.location = 'about:blank'
while(true){
try{
w.origin;
...
if (w.history.length - previous_history_length==2){
... Triggered match
}
}catch(e){
await sleep(100);
}
}
We found that while STTF is being processed, the redirect process is blocked, and if we choose the correct timing, only the last redirect to about:blank will execute immediately. At this point, we can detect how many redirects actually occurred using history.length.
If sleep(TIME) ends before the STTF finishes, we get 2. Otherwise, we get a higher value.
So, to guess the correct value, we need to choose a timing where we consistently get 2 only for the correct prefix.
Timing estimate
Since each user will have different loading times (due to varying internet connections, OS, etc.), I had to implement dynamic timing detection directly in the exploit on the client side.
How it works: Suppose we have a page that contains the text:
Your code: 21346375
So, you need to highlight the text fragment with FOUND='Your cod' and NOT_FOUND='Your coA' (that is, a valid and an invalid prefix before the target text you want to find; it’s also important that neither triggers the boundary check).
After that, I use a binary search to find the appropriate timing.
I simply run the test with the current timing on both the FOUND and NOT_FOUND strings:
- If the function succeeds for both strings, I decrease the upper bound (we are waiting too long)
- If the function finds only one result and it is
FOUND, I increase the lower bound - I repeat this with
(MIN_TIME + MAX_TIME) / 2When the difference between the upper and lower bounds is minimal (in my tests, this difference was 100 milliseconds), I stop the search — we have found the timing.
Tested
This exploit was tested by us on 3 different devices in different internet networks. 2 on MacOS, 1 on Windows 11 – on each device we were able to successfully exfiltrate the code.
Additional Comments
Other issues and limitations of the exploit
The limitations imposed on STTF can be found in bool TextFragmentAnchor::GenerateNewToken:
return loader.LastNavigationHadTransientUserActivation() ||
loader.IsBrowserInitiated();
Therefore, I used onkeydown on the textarea so that while the user is typing, a window appears in the lower right corner.
To hide the popup window and allow the user to continue typing text, I open a new tab in that window and immediately close it, which returns focus to exploit window:
setTimeout(()=>{var a = window.open("about:blank");a.close();},100);
setTimeout(()=>{var a = window.open("about:blank");a.close();},700);
Also, for this exploit to work, the window must be allowed to open other windows. This can also be bypassed, but I didn’t do this to avoid making the PoC even more complicated (since it is already fairly complex). So for testing, make sure your origin is allowed to use window.open.
Summary
STTF allows to leaking text char by char from cross-origin page.
Custom Questions
Reporter credit:
Vsevolod Kokorin (Slonser) of Solidlab and Jorian Woltjer
Additional Data
Category: Security
Chrome Channel: Stable
Regression: N/A \
- https://cross-origin-site/#:~:text=
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/annotation/text_annotation_selector.cc;drc=7d116c4e09471ac9aa11cef21b584a3673db5c76;l=22
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/fragment_directive/text_fragment_anchor.cc;drc=33aa41994c585274ba016ef43a3b8baa3c6334a0;l=86
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/fragment_directive/text_fragment_anchor.cc;drc=7189da78436702acd8f20043b0f394b04f588093;l=114
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/fragment_directive/text_fragment_finder.cc;drc=bd9a480027283342502ac6e044727bb53b2b2c02;l=34