Low watch CVSS 6.5 chrome Sandbox Escape 📄 Reporter bug report 🔧 Commit mapped

Overview

Low
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactPolicy bypass in IFrameSandbox
DescriptionPolicy bypass in IFrameSandbox
ComponentIFrameSandbox
Bug ClassSandbox Escape
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
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.
Tracker483771899
Fix commit874355605d63 (chromium/src) +103/-1
CWECWE-693
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N
CISA KEVNot listed
Credited@Ciarands
Reported2026-02-11 (per vendor advisory)
Fix landed2026-02-20 (public git)
Disclosed2026-04-07
Patch gap46 days — fix public in source before the advisory

Background

iframe sandbox / `allow-popups`
The sandbox attribute restricts an iframe’s capabilities; without the allow-popups token the frame is forbidden from opening new browsing contexts (popups/new tabs).
Emulated Ctrl+click
A synthetic MouseEvent created in script with ctrlKey/metaKey set and dispatched on a link, which the platform would otherwise treat as a request to open the link in a new tab.
`TriggeringEventInfo::kFromUntrustedEvent`
The marker distinguishing a navigation triggered by a script-synthesized (untrusted) DOM event from one triggered by a genuine user gesture.
`FrameLoader::StartNavigation`
The Blink entry point (per the commit subject) that decides whether and how a frame-initiated navigation proceeds, including whether it may open a new browsing context.

Root Cause Analysis

A sandboxed iframe loaded without allow-popups is supposed to be unable to open a new browsing context, but the navigation path did not reject new-context navigations whose triggering event was a script-synthesized “Ctrl+click.” Script inside such a frame could create a MouseEvent with ctrlKey/metaKey set and dispatch it on an anchor, and the resulting navigation request carried a non-kNavigationPolicyCurrentTab policy (open in new tab/window) without the popup-sandbox restriction being enforced at that point.

The fix adds an early guard in FrameLoader::StartNavigation: if the navigation policy is not kNavigationPolicyCurrentTab, the triggering event is TriggeringEventInfo::kFromUntrustedEvent, and the frame IsSandboxed(WebSandboxFlags::kPopups), it returns before the navigation proceeds. This blocks the emulated-gesture popup for popup-restricted sandboxed frames while leaving genuine user gestures and current-tab navigations unaffected. The regression test SandboxedIframePopupCtrlClick confirms the dispatched synthetic Ctrl+click never reaches BeginNavigation (the count stays at 1).

Key insight
The popup-sandbox restriction was not checked against script-synthesized Ctrl/Meta-click navigations, so a sandboxed iframe without allow-popups could emulate a user gesture to open a new browsing context; the fix adds a guard in FrameLoader::StartNavigation that drops untrusted-event, non-current-tab navigations when the frame is popup-sandboxed.

Attack Path

  1. Embed a popup-restricted sandboxed iframe A page includes an iframe with sandbox="allow-scripts" (no allow-popups), for example via srcdoc, so the frame may run script but should not open popups.
  2. Synthesize a Ctrl/Meta click Script in the frame creates a MouseEvent with ctrlKey/metaKey set and dispatches it on an anchor pointing at a URL (the PoC uses about:blank).
  3. Navigation requests a new context (pre-patch) The synthetic click produces a navigation with a non-current-tab policy and kFromUntrustedEvent, which pre-patch was allowed to proceed toward opening a new browsing context despite the missing allow-popups.
  4. Sandbox policy bypass A new top-level browsing context is created from a frame that should be barred from popups; any sandbox restrictions that the new context thereby escapes are (inferred) beyond the popup creation the patch directly addresses.

Impact Assessment

An attacker-controlled page (or third-party content it embeds) could cause a sandboxed iframe lacking allow-popups to open a new browsing context by emulating a Ctrl/Meta click in JavaScript, bypassing the iframe-sandbox popup policy in the renderer. This is reachable by ordinary web content in a stock build with no special flag, extension, or user gesture required; the vendor advisory classifies the component as IFrameSandbox with impact “Policy bypass in IFrameSandbox” at low severity, and any broader sandbox escape beyond the popup-creation bypass is (inferred).

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/core/frame/web_frame_test.cc
modified
IframeBeginNavivationCountTestWebFrameClient
third_party/blink/renderer/core/frame/web_frame_test.cc
modified
TEST_F
third_party/blink/renderer/core/frame/web_frame_test.cc
modified

Files Changed

  • third_party/blink/renderer/core/frame/frame_test_helpers.h
  • third_party/blink/renderer/core/frame/web_frame_test.cc
  • third_party/blink/renderer/core/loader/frame_loader.cc
  • third_party/blink/renderer/core/testing/data/core_test_bundle_data.filelist
  • third_party/blink/renderer/core/testing/data/sandboxed-srcdoc-ctrl-click.html

Audit Directions

  • Trusted vs. untrusted event gating
    Audit navigation/window-open and other gesture-gated capabilities for whether they distinguish kFromUntrustedEvent (script-synthesized) from genuine user activation before granting the capability.
  • Sandbox-flag enforcement points
    Review all code paths that can create a new browsing context (middle-click, Ctrl/Meta-click, window.open, form target) to confirm each consults IsSandboxed(kPopups) rather than relying on one chokepoint.
  • Synthetic modifier-key events
    Search for places where modifier flags on a MouseEvent change navigation disposition, ensuring synthesized events cannot elevate a same-tab navigation into a new-context one inside restricted frames.
From 874355605d632c59770046b1a1fc1582f68361dc Mon Sep 17 00:00:00 2001
From: Nate Chapin <[email protected]>
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 <[email protected]>
Reviewed-by: Daniel Cheng <[email protected]>
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>
Loading diff…

Regression Test / PoC

shipped with the fix
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>
Loading diff…

Original Bug Report

reported by [email protected]

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?

https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/loader/navigation_policy.cc


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') or setWindowOpenHandler) to delegate external link handling to the operating system using shell.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 kNavigationPolicyNewForegroundTab policy), it bypasses renderer-side checks that rely on standard window.open restrictions.
  • 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]

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

View on issue tracker