CVE-2026-5903
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/core/frame/web_frame_test.cc |
modified | |
IframeBeginNavivationCountTestWebFrameClientthird_party/blink/renderer/core/frame/web_frame_test.cc |
modified | |
TEST_Fthird_party/blink/renderer/core/frame/web_frame_test.cc |
modified |
Files Changed
third_party/blink/renderer/core/frame/frame_test_helpers.hthird_party/blink/renderer/core/frame/web_frame_test.ccthird_party/blink/renderer/core/loader/frame_loader.ccthird_party/blink/renderer/core/testing/data/core_test_bundle_data.filelistthird_party/blink/renderer/core/testing/data/sandboxed-srcdoc-ctrl-click.html
Patch
From 874355605d632c59770046b1a1fc1582f68361dc Mon Sep 17 00:00:00 2001
From: Nate Chapin <japhet@chromium.org>
Date: Fri, 20 Feb 2026 11:02:15 -0800
Subject: [PATCH] FrameLoader::StartNavigation should not allow emulated Ctrl+click from sandboxed iframes
Fixed: 483771899
Change-Id: I1e134f0c9dcfbb4760d339d909a7d7339a5e3077
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7572487
Commit-Queue: Nate Chapin <japhet@chromium.org>
Reviewed-by: Daniel Cheng <dcheng@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1587969}
---
diff --git a/third_party/blink/renderer/core/frame/frame_test_helpers.h b/third_party/blink/renderer/core/frame/frame_test_helpers.h
index 740591fa..6b0e55e1 100644
--- a/third_party/blink/renderer/core/frame/frame_test_helpers.h
+++ b/third_party/blink/renderer/core/frame/frame_test_helpers.h
@@ -581,6 +581,14 @@
return sandbox_flags_;
}
+ // Subclasses that override CreateChildFrame() to gave the child frame a
+ // custom TestWebFrameClient subclass lose the propagation of sandbox flags
+ // that is performed in TestWebFrameClient::CreateChildFrame(). This allows
+ // such cases to set the flags manually.
+ void set_sandbox_flags(network::mojom::WebSandboxFlags flags) {
+ sandbox_flags_ = flags;
+ }
+
void DestroyChildViews();
void SetFrameDetachedCallback(base::OnceClosure callback);
diff --git a/third_party/blink/renderer/core/frame/web_frame_test.cc b/third_party/blink/renderer/core/frame/web_frame_test.cc
index c274456..b12511d 100644
--- a/third_party/blink/renderer/core/frame/web_frame_test.cc
+++ b/third_party/blink/renderer/core/frame/web_frame_test.cc
@@ -7653,8 +7653,8 @@
// frame_test_helpers::TestWebFrameClient:
void BeginNavigation(std::unique_ptr<WebNavigationInfo> info) override {
+ begin_navigation_call_count_++;
if (ignore_navigations_) {
- begin_navigation_call_count_++;
return;
}
TestWebFrameClient::BeginNavigation(std::move(info));
@@ -14845,5 +14845,56 @@
EXPECT_EQ(old_parent->ConnectedSubframeCount(), 0u);
EXPECT_EQ(new_parent->ConnectedSubframeCount(), 1u);
}
+class IframeBeginNavivationCountTestWebFrameClient
+ : public frame_test_helpers::TestWebFrameClient {
+ public:
+ IframeBeginNavivationCountTestWebFrameClient() = default;
+ ~IframeBeginNavivationCountTestWebFrameClient() override = default;
+
+ // WebLocalFrameClient:
+ WebLocalFrame* CreateChildFrame(
+ mojom::blink::TreeScopeType scope,
+ const WebString& name,
+ const WebString& fallback_name,
+ const FramePolicy& frame_policy,
+ const WebFrameOwnerProperties&,
+ FrameOwnerElementType,
+ WebPolicyContainerBindParams policy_container_bind_params,
+ ukm::SourceId document_ukm_source_id,
+ FinishChildFrameCreationFn finish_creation) override {
+ auto client = std::make_unique<TestNewWindowWebFrameClient>();
+ client_ = client.get();
+ client_->set_sandbox_flags(frame_policy.sandbox_flags);
+ return CreateLocalChild(*Frame(), scope, std::move(client),
+ std::move(policy_container_bind_params),
+ finish_creation);
+ }
+
+ TestNewWindowWebFrameClient* iframe_client() const { return client_; }
+
+ private:
+ TestNewWindowWebFrameClient* client_ = nullptr;
+};
+
+TEST_F(WebFrameTest, SandboxedIframePopupCtrlClick) {
+ RegisterMockedHttpURLLoad("sandboxed-srcdoc-ctrl-click.html");
+ IframeBeginNavivationCountTestWebFrameClient web_frame_client;
+ frame_test_helpers::WebViewHelper web_view_helper;
+ web_view_helper.InitializeAndLoad(
+ base_url_ + "sandboxed-srcdoc-ctrl-click.html", &web_frame_client);
+
+ ASSERT_EQ(web_frame_client.iframe_client()->BeginNavigationCallCount(), 1);
+
+ LocalFrame* child = To<LocalFrame>(
+ web_view_helper.GetWebView()->GetPage()->MainFrame()->FirstChild());
+ Element* element =
+ child->GetDocument()->body()->getElementById(AtomicString("btn"));
+ To<HTMLElement>(element)->click();
+
+ // Clicking the button will attempt a synthetic Ctrl+Click from an iframe
+ // sandboxed without `allow-popups`. This should be blocked before reaching
+ // BeginNavigation().
+ EXPECT_EQ(web_frame_client.iframe_client()->BeginNavigationCallCount(), 1);
+}
} // namespace blink
diff --git a/third_party/blink/renderer/core/loader/frame_loader.cc b/third_party/blink/renderer/core/loader/frame_loader.cc
index 64e74d0..f413c08d 100644
--- a/third_party/blink/renderer/core/loader/frame_loader.cc
+++ b/third_party/blink/renderer/core/loader/frame_loader.cc
@@ -868,6 +868,17 @@
return;
}
+ // A sandboxed iframe without `allow-popups` should not be able to
+ // open a new browsing context by emulating a user gesture in JS.
+ // (e.g. Ctrl+click).
+ if (request.GetNavigationPolicy() != kNavigationPolicyCurrentTab &&
+ request.GetTriggeringEventInfo() ==
+ mojom::blink::TriggeringEventInfo::kFromUntrustedEvent &&
+ frame_->GetSecurityContext()->IsSandboxed(
+ network::mojom::blink::WebSandboxFlags::kPopups)) {
+ return;
+ }
+
if (request.GetNavigationPolicy() == kNavigationPolicyCurrentTab &&
(!origin_window || origin_window->GetSecurityOrigin()->CanAccess(
frame_->DomWindow()->GetSecurityOrigin()))) {
diff --git a/third_party/blink/renderer/core/testing/data/core_test_bundle_data.filelist b/third_party/blink/renderer/core/testing/data/core_test_bundle_data.filelist
index d20416c3..03dd574b0 100644
--- a/third_party/blink/renderer/core/testing/data/core_test_bundle_data.filelist
+++ b/third_party/blink/renderer/core/testing/data/core_test_bundle_data.filelist
@@ -433,6 +433,7 @@
testing/data/rtl-iframe-inner.html
testing/data/rtl-iframe.html
testing/data/rtl-overview-mode.html
+testing/data/sandboxed-srcdoc-ctrl-click.html
testing/data/scale_oscillate.html
testing/data/script/nested-script.js
testing/data/script/not-parser-blocked-async-nested-script.html
diff --git a/third_party/blink/renderer/core/testing/data/sandboxed-srcdoc-ctrl-click.html b/third_party/blink/renderer/core/testing/data/sandboxed-srcdoc-ctrl-click.html
new file mode 100644
index 0000000..b325b44
--- /dev/null
+++ b/third_party/blink/renderer/core/testing/data/sandboxed-srcdoc-ctrl-click.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<html lang="en">
+<body>
+<iframe
+ sandbox="allow-scripts"
+ srcdoc='
+ <div id="container"></div>
+ <script>
+ let clicked = false;
+ const btn = document.createElement("a");
+ btn.id = "btn";
+ btn.href = "about:blank";
+ document.body.appendChild(btn);
+
+ btn.addEventListener("click", (event) => {
+ if (clicked) return;
+ clicked = true;
+ event.preventDefault();
+ const clickEvent = new MouseEvent("click", {
+ ctrlKey: true,
+ metaKey: true,
+ bubbles: true,
+ cancelable: true,
+ });
+ btn.dispatchEvent(clickEvent);
+ });
+ </script>
+'
+>
+</iframe>
+</html>
Regression Test / PoC
diff --git a/third_party/blink/renderer/core/frame/web_frame_test.cc b/third_party/blink/renderer/core/frame/web_frame_test.cc
index c274456..b12511d 100644
--- a/third_party/blink/renderer/core/frame/web_frame_test.cc
+++ b/third_party/blink/renderer/core/frame/web_frame_test.cc
@@ -7653,8 +7653,8 @@
// frame_test_helpers::TestWebFrameClient:
void BeginNavigation(std::unique_ptr<WebNavigationInfo> info) override {
+ begin_navigation_call_count_++;
if (ignore_navigations_) {
- begin_navigation_call_count_++;
return;
}
TestWebFrameClient::BeginNavigation(std::move(info));
@@ -14845,5 +14845,56 @@
EXPECT_EQ(old_parent->ConnectedSubframeCount(), 0u);
EXPECT_EQ(new_parent->ConnectedSubframeCount(), 1u);
}
+class IframeBeginNavivationCountTestWebFrameClient
+ : public frame_test_helpers::TestWebFrameClient {
+ public:
+ IframeBeginNavivationCountTestWebFrameClient() = default;
+ ~IframeBeginNavivationCountTestWebFrameClient() override = default;
+
+ // WebLocalFrameClient:
+ WebLocalFrame* CreateChildFrame(
+ mojom::blink::TreeScopeType scope,
+ const WebString& name,
+ const WebString& fallback_name,
+ const FramePolicy& frame_policy,
+ const WebFrameOwnerProperties&,
+ FrameOwnerElementType,
+ WebPolicyContainerBindParams policy_container_bind_params,
+ ukm::SourceId document_ukm_source_id,
+ FinishChildFrameCreationFn finish_creation) override {
+ auto client = std::make_unique<TestNewWindowWebFrameClient>();
+ client_ = client.get();
+ client_->set_sandbox_flags(frame_policy.sandbox_flags);
+ return CreateLocalChild(*Frame(), scope, std::move(client),
+ std::move(policy_container_bind_params),
+ finish_creation);
+ }
+
+ TestNewWindowWebFrameClient* iframe_client() const { return client_; }
+
+ private:
+ TestNewWindowWebFrameClient* client_ = nullptr;
+};
+
+TEST_F(WebFrameTest, SandboxedIframePopupCtrlClick) {
+ RegisterMockedHttpURLLoad("sandboxed-srcdoc-ctrl-click.html");
+ IframeBeginNavivationCountTestWebFrameClient web_frame_client;
+ frame_test_helpers::WebViewHelper web_view_helper;
+ web_view_helper.InitializeAndLoad(
+ base_url_ + "sandboxed-srcdoc-ctrl-click.html", &web_frame_client);
+
+ ASSERT_EQ(web_frame_client.iframe_client()->BeginNavigationCallCount(), 1);
+
+ LocalFrame* child = To<LocalFrame>(
+ web_view_helper.GetWebView()->GetPage()->MainFrame()->FirstChild());
+ Element* element =
+ child->GetDocument()->body()->getElementById(AtomicString("btn"));
+ To<HTMLElement>(element)->click();
+
+ // Clicking the button will attempt a synthetic Ctrl+Click from an iframe
+ // sandboxed without `allow-popups`. This should be blocked before reaching
+ // BeginNavigation().
+ EXPECT_EQ(web_frame_client.iframe_client()->BeginNavigationCallCount(), 1);
+}
} // namespace blink
diff --git a/third_party/blink/renderer/core/testing/data/core_test_bundle_data.filelist b/third_party/blink/renderer/core/testing/data/core_test_bundle_data.filelist
index d20416c3..03dd574b0 100644
--- a/third_party/blink/renderer/core/testing/data/core_test_bundle_data.filelist
+++ b/third_party/blink/renderer/core/testing/data/core_test_bundle_data.filelist
@@ -433,6 +433,7 @@
testing/data/rtl-iframe-inner.html
testing/data/rtl-iframe.html
testing/data/rtl-overview-mode.html
+testing/data/sandboxed-srcdoc-ctrl-click.html
testing/data/scale_oscillate.html
testing/data/script/nested-script.js
testing/data/script/not-parser-blocked-async-nested-script.html
diff --git a/third_party/blink/renderer/core/testing/data/sandboxed-srcdoc-ctrl-click.html b/third_party/blink/renderer/core/testing/data/sandboxed-srcdoc-ctrl-click.html
new file mode 100644
index 0000000..b325b44
--- /dev/null
+++ b/third_party/blink/renderer/core/testing/data/sandboxed-srcdoc-ctrl-click.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<html lang="en">
+<body>
+<iframe
+ sandbox="allow-scripts"
+ srcdoc='
+ <div id="container"></div>
+ <script>
+ let clicked = false;
+ const btn = document.createElement("a");
+ btn.id = "btn";
+ btn.href = "about:blank";
+ document.body.appendChild(btn);
+
+ btn.addEventListener("click", (event) => {
+ if (clicked) return;
+ clicked = true;
+ event.preventDefault();
+ const clickEvent = new MouseEvent("click", {
+ ctrlKey: true,
+ metaKey: true,
+ bubbles: true,
+ cancelable: true,
+ });
+ btn.dispatchEvent(clickEvent);
+ });
+ </script>
+'
+>
+</iframe>
+</html>
Original Bug Report
HTML5 Sandbox Security Model Violation with auxiliary browsing contexts being created despite the lack of "allow-popups" keyword within iframes
Report description
HTML5 Sandbox Security Model Violation with auxiliary browsing contexts being created despite the lack of “allow-popups” keyword within iframes
Bug location
Where do you want to report your vulnerability?
Chrome VRP – Report security issues affecting the Chrome browser. See program rules
Which URL (or repository) have you found the vulnerability in?
The problem
Please describe the technical details of the vulnerability
The vulnerability exists within the Blink rendering engine’s handling of navigation policies derived from mouse events. Specifically, the function NavigationPolicyFromEventInternal in third_party/blink/renderer/core/loader/navigation_policy.cc fails to validate the isTrusted property of a MouseEvent before honoring modifier keys (such as ctrlKey or metaKey).
In a standard HTML5 sandbox environment, an iframe without the allow-popups flag is prohibited from opening new auxiliary browsing contexts (windows or tabs). However, this restriction relies on the renderer correctly calculating the Navigation Policy for a given action.
When a user performs a Ctrl+Click (or Cmd+Click on macOS) on a link, Blink translates this into a navigation policy of kNavigationPolicyNewBackgroundTab or kNavigationPolicyNewForegroundTab. The vulnerability arises because JavaScript within a sandboxed iframe can programmatically construct a synthetic MouseEvent with these modifier keys set to true and dispatch it against an anchor element.
Because NavigationPolicyFromEventInternal does not check if the event is trusted (event.isTrusted()), it blindly processes the synthetic modifiers. Consequently, the renderer calculates a “New Tab” navigation policy and requests the browser to open a new window. This effectively bypasses the allow-popups restriction, as the browser treats the request as a legitimate link navigation with a specific disposition, rather than a blocked window.open call.
Impact analysis
This vulnerability is a security bypass of the HTML5 sandbox allow-popups restriction. While typically classified as a sandbox escape in standard browsers (allowing untrusted content to annoy users or facilitate phishing via popups), the impact is significantly amplified in certain Electron-based applications, such as the Discord Desktop Client, where it can serve as a primitive for Remote Code Execution (RCE).
Core Impact: Sandbox Escape & Security Control Bypass
The vulnerability allows sandboxed content (explicitly restricted from creating new windows) to force the creation of an auxiliary browsing context. This directly violates the integrity of the sandbox attribute, rendering the allow-popups flag ineffective against a malicious actor capable of executing JavaScript.
High-Severity Exploitation in Electron (Discord RCE Chain)
In the context of the Discord Desktop Client, this sandbox escape bridges the gap between a restricted renderer process and privileged system operations.
- Mechanism: Electron applications often intercept new window creation events (such as via
webContents.on('new-window')orsetWindowOpenHandler) to delegate external link handling to the operating system usingshell.openExternal(). - The Bypass: Discord implements checks to prevent sandboxed iframes from triggering this flow. However, because the vulnerability forces the Blink engine to classify the synthetic event as a legitimate, user-initiated “New Tab” navigation (via the
kNavigationPolicyNewForegroundTabpolicy), it bypasses renderer-side checks that rely on standardwindow.openrestrictions. - RCE Vector: By successfully triggering
shell.openExternal()with a malicious payload, we demonstrated the ability to execute arbitrary code on the host machine.- Violation: This directly violates the Electron security best practice: " Do not use shell.openExternal with untrusted content". Electron Checklist #15
- Chain:
Sandboxed Iframe->Synthetic Ctrl+Click->Sandbox Bypass (New Window Request)->Electron Interception->shell.openExternal(malicious_URI)->RCE.
This finding confirms that the “One Permitted Navigator” principle and standard event validation are insufficient in their current state to protect embedding contexts from synthetic input attacks.
The cause
What version of Chrome have you found the security issue in?
[145.0.7632.46]
Is the security issue related to a crash?
No, it is not related to a crash.
Choose the type of vulnerability
Sandbox Escape
How would you like to be publicly acknowledged for your report?
Credit to my GitHub @Ciarands
- https://bughunters.google.com/about/rules/5745167867576320/chrome-vulnerability-reward-program-rules
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/loader/navigation_policy.cc
- https://www.electronjs.org/docs/latest/tutorial/security#15-do-not-use-shellopenexternal-with-untrusted-content