CVE-2026-5891
Overview
Files Changed
content/browser/bad_message.hcontent/browser/renderer_host/render_frame_host_impl.cccontent/browser/renderer_host/render_frame_host_impl.hcontent/browser/security_exploit_browsertest.cctools/metrics/histograms/metadata/stability/enums.xml
Patch
From a12e651f6f62614ab4a0c0eed6d686fec8414682 Mon Sep 17 00:00:00 2001
From: Alex Moshchuk <alexmos@chromium.org>
Date: Sat, 28 Feb 2026 11:18:02 -0800
Subject: [PATCH] Add browser-side validation for allow-popups sandboxed frame attribute
Sandboxed frames created without an allow-popups attribute should not
be allowed to create popups. Previously, this attribute was only
checked in the renderer. This CL adds additional validation in
RenderFrameHostImpl::CreateNewWindow(), the browser process's
entrypoint for creating popups via web APIs like window.open().
Bug: 487471101
Change-Id: I0776f9d6df4200ecf20b29e8ecd18efa2edcd45a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7617636
Reviewed-by: Mark Pearson <mpearson@chromium.org>
Auto-Submit: Alex Moshchuk <alexmos@chromium.org>
Commit-Queue: Mark Pearson <mpearson@chromium.org>
Reviewed-by: Charlie Reis <creis@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1592057}
---
diff --git a/content/browser/bad_message.h b/content/browser/bad_message.h
index 6873983..372417d 100644
--- a/content/browser/bad_message.h
+++ b/content/browser/bad_message.h
@@ -360,6 +360,7 @@
RFH_INVALID_DOCUMENT_SEQUENCE_NUMBER = 332,
RFH_NEW_ISOLATED_WEB_APP_PERMISSION_POLICIES = 333,
RFH_CREATE_NEW_WINDOW_INVALID_DISPOSITION = 334,
+ RFH_CREATE_NEW_WINDOW_FROM_SANDBOXED_FRAME = 335,
// Please add new elements here. The naming convention is abbreviated class
// name (e.g. RenderFrameHost becomes RFH) plus a unique description of the
diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc
index a71222b..b5cd09d 100644
--- a/content/browser/renderer_host/render_frame_host_impl.cc
+++ b/content/browser/renderer_host/render_frame_host_impl.cc
@@ -9968,6 +9968,16 @@
return;
}
+ // Sandboxed frames should only be allowed to create a popup when they have
+ // the "allow-popups" attribute. This should have already been checked by the
+ // renderer process (see blink::CreateNewWindow()), and this browser-side
+ // check defends against compromised renderers.
+ if (IsSandboxed(network::mojom::WebSandboxFlags::kPopups)) {
+ bad_message::ReceivedBadMessage(
+ GetProcess(), bad_message::RFH_CREATE_NEW_WINDOW_FROM_SANDBOXED_FRAME);
+ return;
+ }
+
// Fenced frames that have revoked network access can't open popups.
if (base::FeatureList::IsEnabled(
blink::features::kFencedFramesLocalUnpartitionedDataAccess)) {
diff --git a/content/browser/renderer_host/render_frame_host_impl.h b/content/browser/renderer_host/render_frame_host_impl.h
index e7821c9b..81c5099 100644
--- a/content/browser/renderer_host/render_frame_host_impl.h
+++ b/content/browser/renderer_host/render_frame_host_impl.h
@@ -3466,6 +3466,8 @@
AttemptDuplicateRenderWidgetHost);
FRIEND_TEST_ALL_PREFIXES(SecurityExploitBrowserTest,
BindToWebUIFromWebViaMojo);
+ FRIEND_TEST_ALL_PREFIXES(SecurityExploitBrowserTest,
+ WindowOpenDisallowedFromSandboxedFrame);
FRIEND_TEST_ALL_PREFIXES(SitePerProcessBrowserTest,
RenderViewHostIsNotReusedAfterDelayedUnloadACK);
FRIEND_TEST_ALL_PREFIXES(SitePerProcessBrowserTest,
diff --git a/content/browser/security_exploit_browsertest.cc b/content/browser/security_exploit_browsertest.cc
index 4d0c4617..802f0ad 100644
--- a/content/browser/security_exploit_browsertest.cc
+++ b/content/browser/security_exploit_browsertest.cc
@@ -1111,6 +1111,55 @@
kill_waiter.Wait());
}
+// Regression test for browser-side validation of the allow-popups sandbox
+// attribute. See https://crbug.com/487471101.
+IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
+ WindowOpenDisallowedFromSandboxedFrame) {
+ // Explicitly isolating b.com helps establish consistent expectations for the
+ // sandboxed frame across platforms.
+ IsolateOrigin("b.com");
+
+ GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
+ EXPECT_TRUE(NavigateToURL(shell(), main_url));
+
+ WebContentsImpl* web_contents =
+ static_cast<WebContentsImpl*>(shell()->web_contents());
+ FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();
+ RenderFrameHostImpl* main_frame = root->current_frame_host();
+
+ // Create cross-site sandboxed child frame. The frame lacks the allow-popups
+ // attribute, so it should not be allowed to create popups.
+ GURL child_url(embedded_test_server()->GetURL("b.com", "/title2.html"));
+ {
+ std::string js_str = base::StringPrintf(
+ "var frame = document.createElement('iframe'); "
+ "frame.sandbox = 'allow-scripts'; "
+ "frame.src = '%s'; "
+ "document.body.appendChild(frame);",
+ child_url.spec().c_str());
+ EXPECT_TRUE(ExecJs(main_frame, js_str));
+ ASSERT_TRUE(WaitForLoadStop(web_contents));
+ }
+
+ RenderFrameHostImpl* subframe = root->child_at(0)->current_frame_host();
+ EXPECT_TRUE(
+ subframe->GetSiteInstance()->GetSecurityPrincipal().IsSandboxed());
+ EXPECT_TRUE(subframe->IsSandboxed(network::mojom::WebSandboxFlags::kPopups));
+
+ // Simulate that the b.com renderer is compromised and sends an IPC to open a
+ // popup, bypassing renderer-side checks in blink::CreateNewWindow(). The
+ // browser process should detect this and terminate the renderer.
+ RenderProcessHostBadIpcMessageWaiter kill_waiter(subframe->GetProcess());
+ mojom::CreateNewWindowParamsPtr params = mojom::CreateNewWindowParams::New();
+ params->target_url = GURL("about:blank");
+ params->disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB;
+ subframe->CreateNewWindow(std::move(params), base::DoNothing());
+ EXPECT_EQ(bad_message::RFH_CREATE_NEW_WINDOW_FROM_SANDBOXED_FRAME,
+ kill_waiter.Wait());
+ EXPECT_FALSE(subframe->IsRenderFrameLive());
+ EXPECT_EQ(1u, Shell::windows().size());
+}
+
// Test verifying that a compromised renderer can't lie about the source_origin
// passed along with the RouteMessageEvent() mojo message. Similar to the test
// above, but exercises a scenario where the source origin is opaque and the
diff --git a/tools/metrics/histograms/metadata/stability/enums.xml b/tools/metrics/histograms/metadata/stability/enums.xml
index bfaf4c1..de97da0 100644
--- a/tools/metrics/histograms/metadata/stability/enums.xml
+++ b/tools/metrics/histograms/metadata/stability/enums.xml
@@ -500,6 +500,7 @@
<int value="332" label="RFH_INVALID_DOCUMENT_SEQUENCE_NUMBER"/>
<int value="333" label="RFH_NEW_ISOLATED_WEB_APP_PERMISSION_POLICIES"/>
<int value="334" label="RFH_CREATE_NEW_WINDOW_INVALID_DISPOSITION"/>
+ <int value="335" label="RFH_CREATE_NEW_WINDOW_FROM_SANDBOXED_FRAME"/>
</enum>
<enum name="BadMessageReasonExtensions">
Regression Test / PoC
diff --git a/content/browser/security_exploit_browsertest.cc b/content/browser/security_exploit_browsertest.cc
index 4d0c4617..802f0ad 100644
--- a/content/browser/security_exploit_browsertest.cc
+++ b/content/browser/security_exploit_browsertest.cc
@@ -1111,6 +1111,55 @@
kill_waiter.Wait());
}
+// Regression test for browser-side validation of the allow-popups sandbox
+// attribute. See https://crbug.com/487471101.
+IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
+ WindowOpenDisallowedFromSandboxedFrame) {
+ // Explicitly isolating b.com helps establish consistent expectations for the
+ // sandboxed frame across platforms.
+ IsolateOrigin("b.com");
+
+ GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
+ EXPECT_TRUE(NavigateToURL(shell(), main_url));
+
+ WebContentsImpl* web_contents =
+ static_cast<WebContentsImpl*>(shell()->web_contents());
+ FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();
+ RenderFrameHostImpl* main_frame = root->current_frame_host();
+
+ // Create cross-site sandboxed child frame. The frame lacks the allow-popups
+ // attribute, so it should not be allowed to create popups.
+ GURL child_url(embedded_test_server()->GetURL("b.com", "/title2.html"));
+ {
+ std::string js_str = base::StringPrintf(
+ "var frame = document.createElement('iframe'); "
+ "frame.sandbox = 'allow-scripts'; "
+ "frame.src = '%s'; "
+ "document.body.appendChild(frame);",
+ child_url.spec().c_str());
+ EXPECT_TRUE(ExecJs(main_frame, js_str));
+ ASSERT_TRUE(WaitForLoadStop(web_contents));
+ }
+
+ RenderFrameHostImpl* subframe = root->child_at(0)->current_frame_host();
+ EXPECT_TRUE(
+ subframe->GetSiteInstance()->GetSecurityPrincipal().IsSandboxed());
+ EXPECT_TRUE(subframe->IsSandboxed(network::mojom::WebSandboxFlags::kPopups));
+
+ // Simulate that the b.com renderer is compromised and sends an IPC to open a
+ // popup, bypassing renderer-side checks in blink::CreateNewWindow(). The
+ // browser process should detect this and terminate the renderer.
+ RenderProcessHostBadIpcMessageWaiter kill_waiter(subframe->GetProcess());
+ mojom::CreateNewWindowParamsPtr params = mojom::CreateNewWindowParams::New();
+ params->target_url = GURL("about:blank");
+ params->disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB;
+ subframe->CreateNewWindow(std::move(params), base::DoNothing());
+ EXPECT_EQ(bad_message::RFH_CREATE_NEW_WINDOW_FROM_SANDBOXED_FRAME,
+ kill_waiter.Wait());
+ EXPECT_FALSE(subframe->IsRenderFrameLive());
+ EXPECT_EQ(1u, Shell::windows().size());
+}
+
// Test verifying that a compromised renderer can't lie about the source_origin
// passed along with the RouteMessageEvent() mojo message. Similar to the test
// above, but exercises a scenario where the source origin is opaque and the
Original Bug Report
Sandboxed iframe bypasses allow-popups restriction via CreateNewWindow Mojo IPC — browser has zero sandbox enforcement
Report description
Sandboxed iframe bypasses allow-popups restriction via CreateNewWindow Mojo IPC — browser has zero sandbox enforcement
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
RenderFrameHostImpl::CreateNewWindow() in content/browser/renderer_host/render_frame_host_impl.cc does not check the allow-popups sandbox flag (WebSandboxFlags::kPopups) before opening a new window. The sandbox enforcement for popup creation is only performed on the renderer side in create_window.cc. A compromised renderer in a sandboxed iframe can call the CreateNewWindow Mojo IPC directly with allow_popup=true, bypassing all Blink-side sandbox enforcement and opening arbitrary popups.
Vulnerable files:
- Browser: content/browser/renderer_host/render_frame_host_impl.cc
- Renderer sandbox check (bypassed): third_party/blink/renderer/core/page/create_window.cc
- Renderer sets allow_popup: content/renderer/render_frame_impl.cc
- Mojom definition: third_party/blink/public/mojom/frame/frame.mojom (
CreateNewWindowParams.allow_popup)
Transmission chain:
- A sandboxed
<iframe sandbox="allow-scripts allow-same-origin">(withoutallow-popups) contains a compromised renderer. - The renderer patches out the Blink-side
IsSandboxed(kPopups)check increate_window.cc:347-358— the only enforcement point — and setsparams->allow_popup = trueunconditionally inrender_frame_impl.cc:6779-6781. RenderFrameHostImpl::CreateNewWindow()receives the IPC. It computeseffective_transient_activation_stateby OR-ingparams->allow_popupdirectly:
// render_frame_host_impl.cc:9994-9997
bool effective_transient_activation_state =
params->allow_popup || HasTransientUserActivation() ||
(transient_allow_popup_.IsActive() &&
params->disposition == WindowOpenDisposition::NEW_POPUP);
- Since
params->allow_popup = true,effective_transient_activation_stateistrueregardless of any sandbox restrictions. CanCreateWindow()is called witheffective_transient_activation_state=true— popup is allowed. NoIsSandboxed()check anywhere in the browser handler.- The new window opens in full non-sandboxed browser context.
The renderer-side sandbox check in create_window.cc:
// create_window.cc:347-358
if (opener_window.IsSandboxed(
network::mojom::blink::WebSandboxFlags::kPopups)) {
opener_window.AddConsoleMessage(...
"Blocked opening '...' in a new window because the request was made "
"in a sandboxed frame whose 'allow-popups' permission is not set.");
return nullptr;
}
…is Blink renderer code only. The browser-side CreateNewWindow() handler has no equivalent check — IsSandboxed() and WebSandboxFlags::kPopups do not appear anywhere in the method.
Steps to reproduce:
- Apply
h6_patch_renderer_poc.difftochromium-stable/src/(Chrome 145.0.7632.117):cd chromium-stable/src git apply h6_patch_renderer_poc.diff - Build Chrome:
autoninja -C out/Default chrome - Start the PoC HTTP server:
python3 serve.py - Launch the patched Chrome:
out/Default/Chromium.app/Contents/MacOS/Chromium \ --user-data-dir=/tmp/chrome-h6-test \ http://localhost:8080/index.html - After 1 second,
pwned.htmlopens in a new popup window — despite the iframe havingsandbox="allow-scripts allow-same-origin"with noallow-popups.
h6_patch_renderer_poc.diff simulates a compromised renderer by (1) removing the IsSandboxed(kPopups) check from create_window.cc and (2) forcing params->allow_popup = true unconditionally in render_frame_impl.cc. In practice, a compromised renderer would call the CreateNewWindow Mojo IPC directly with allow_popup=true, skipping Blink entirely.
Bisect:
Introducing commit: c4cb716e319efaa1c2582565c5f18965dfa39366
- Author: Mustaq Ahmed (mustaq@google.com)
- Date: Tue Jun 05, 2018
- Message: “Browser-side activation state with sync across OOPIFs”
- CL: https://chromium-review.googlesource.com/967260
- Cr-Commit-Position: refs/heads/master@{#564530}
- Bugs: 780556, 775930
Evidence:
- Gitiles blame on
render_frame_host_impl.ccline 9994 (params->allow_popup || HasTransientUserActivation()) traces directly to this commit. - This CL introduced
effective_transient_activation_statewhich OR-s the renderer-suppliedmimic_user_gesture(later renamedallow_popup) into the browser’s activation decision, with noIsSandboxed(kPopups)check. - The field was later renamed
mimic_user_gesture→allow_popupin commit8601a5646d6e(Mustaq Ahmed, 2020-01-31, CL: https://chromium-review.googlesource.com/c/chromium/src/+/2031187, refs/heads/master@{#737378}), but the missing browser-side sandbox check persisted. - The renderer-side
IsSandboxed(kPopups)check increate_window.cchas existed since the early Blink sandbox implementation and was never mirrored browser-side.
Earliest affected: Chrome M68 (stable July 2018, when c4cb716e landed). Latest confirmed: Chrome M145 (145.0.7632.117, current Stable). Vulnerable for ~7.5 years.
Suggested fix:
I will upload a Gerrit CL with this fix to chromium-review.googlesource.com and add the link in a follow-up comment. h6_fix.diff is attached for reference.
Add a browser-side sandbox check in RenderFrameHostImpl::CreateNewWindow(), after the fenced frame check and before effective_transient_activation_state is computed:
+ // Sandboxed frames without allow-popups cannot open new windows.
+ // This enforces the kPopups sandbox flag browser-side; the renderer-side
+ // check in create_window.cc can be bypassed by a compromised renderer
+ // sending allow_popup=true in the Mojo params.
+ if (IsSandboxed(network::mojom::WebSandboxFlags::kPopups)) {
+ bad_message::ReceivedBadMessage(
+ GetProcess(),
+ bad_message::RFH_CREATE_NEW_WINDOW_FROM_SANDBOXED_FRAME);
+ std::move(callback).Run(mojom::CreateNewWindowStatus::kBlocked, nullptr);
+ return;
+ }
And in bad_message.h, after RFH_CREATE_NEW_WINDOW_INVALID_DISPOSITION = 334:
+ RFH_CREATE_NEW_WINDOW_FROM_SANDBOXED_FRAME = 335,
This mirrors the renderer-side check but enforces it browser-side with ReceivedBadMessage to kill the compromised renderer. IsSandboxed() uses the browser’s own active_sandbox_flags() which cannot be forged by a compromised renderer.
Verified: applying both h6_patch_renderer_poc.diff and h6_fix.diff, the exploit is blocked and the renderer process is terminated. Normal popups from non-sandboxed frames still work correctly.
Impact analysis
A compromised renderer inside a sandboxed iframe (e.g., a sandboxed ad frame with sandbox="allow-scripts allow-same-origin") can open arbitrary popup windows, bypassing the allow-popups sandbox restriction.
Exploitation requires a compromised renderer (renderer RCE), which is a standard assumption in Chrome’s threat model. The attacker gains the ability to:
- Open arbitrary popups from content that is explicitly sandboxed to prevent popup creation
- Bypass the iframe sandbox contract that embedders rely on to restrict ad/third-party content
- UI spoofing and phishing — a sandboxed ad frame could open convincing browser windows mimicking security dialogs or login pages
- Popup flood attacks — repeatedly open windows from a sandboxed context that should not have this capability
- Bypass permission-policy enforcement — sites use
allow-popupsrestriction to prevent untrusted third-party content from opening new browsing contexts
The allow-popups sandbox flag exists specifically to prevent untrusted content from opening new windows. The browser’s failure to enforce this flag means the sandbox guarantee is illusory against a compromised renderer.
This is the same vulnerability class as H4 (download sandbox bypass via DownloadURL) and H1 (user gesture spoofing), both previously accepted. The pattern: renderer-only enforcement of a WebSandboxFlags restriction, with the browser unconditionally trusting a renderer-supplied parameter.
The cause
What version of Chrome have you found the security issue in?
145.0.7632.117 (Stable, current as of February 25, 2026)
Is the security issue related to a crash?
No, it is not related to a crash.
Choose the type of vulnerability
Site Isolation Bypass
How would you like to be publicly acknowledged for your report?
Tianyi Hu
- http://localhost:8080/index.html
- https://bughunters.google.com/about/rules/5745167867576320/chrome-vulnerability-reward-program-rules
- https://chromium-review.googlesource.com/967260
- https://chromium-review.googlesource.com/c/chromium/src/+/2031187
- https://source.chromium.org/chromium/chromium/src/+/main:content/browser/renderer_host/render_frame_host_impl.cc
- https://source.chromium.org/chromium/chromium/src/+/main:content/renderer/render_frame_impl.cc
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/public/mojom/frame/frame.mojom
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/page/create_window.cc