CVE-2026-7934
Overview
Background
- Popup blocker
- Chrome’s defense that suppresses
window.open/new-window requests not backed by a user gesture, unless a privileged context (extension, hosted app, content-script-injected page) is allowed to bypass it. - `CreateNewWindowParams::allow_popup`
- a boolean in the renderer-to-browser IPC that requests bypassing the popup blocker.
- `ContentBrowserClient::IsPopupBypassAllowed`
- the new browser-side hook this CL introduces so the browser can independently verify a bypass request instead of trusting the renderer.
- `ProcessMap` / `ScriptInjectionTracker`
- browser-side bookkeeping of which extensions own a process, what context types a process may host, and which extensions have run content scripts in a process.
Root Cause Analysis
Per the commit message, RenderFrameHostImpl::CreateNewWindow() trusted the renderer-supplied allow_popup field in CreateNewWindowParams to bypass the popup blocker, and the gating was only enforced renderer-side in ExtensionsRendererClient::AllowPopup(). A compromised renderer can synthesize an IPC with allow_popup = true regardless of whether the process is actually privileged, so the browser would grant a popup-blocker bypass to an ordinary web page — a browser-side input-validation failure that trusts a security-relevant flag from an untrusted process.
The fix adds a browser-side authority: ChromeContentBrowserClient::IsPopupBypassAllowed independently verifies (using ProcessMap::GetEnabledExtensionByProcessID / CanProcessHostContextType and ScriptInjectionTracker) that the process genuinely hosts an authorized extension (kPrivilegedExtension), a privileged web page such as a hosted app in an outermost main frame (kPrivilegedWebPage), or has had an extension content script injected, returning false otherwise. Per the commit message CreateNewWindow() now requires this method to agree with the allow_popup flag, and the renderer-side AllowPopup() checks are also hardened; the browser no longer takes the renderer’s word. Note the diff’s non-test source hunks only show chrome_content_browser_client.cc/.h; the RenderFrameHostImpl::CreateNewWindow and ExtensionsRendererClient::AllowPopup edits are described in the commit message but are not present in the shown source.
allow_popup IPC flag to grant a popup-blocker bypass; the fix adds browser-side verification via IsPopupBypassAllowed that the originating process is genuinely an extension/hosted-app/content-script context before honoring the bypass.Attack Path
- Compromise a renderer An attacker gains code execution in a non-privileged (ordinary web) renderer process (inferred precondition stated in the commit message).
- Forge the bypass flag
The compromised renderer sends a
CreateNewWindowIPC withCreateNewWindowParams::allow_popupset totrue, which the regression test models withPwnMessageHelper::OpenPopup. - Browser trusts the flag (pre-patch)
RenderFrameHostImpl::CreateNewWindow()honorsallow_popupwithout independent verification, so the popup blocker is bypassed from a process that has no legitimate privilege. - Open unsolicited popups
The renderer opens windows/popups without a user gesture, defeating the popup blocker (post-patch,
IsPopupBypassAllowedreturnsfalsefor such a process and the bypass is denied).
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/chrome_content_browser_client.cc |
modified | |
TEST_Fchrome/browser/chrome_content_browser_client_unittest.cc |
modified |
Files Changed
chrome/browser/chrome_content_browser_client.ccchrome/browser/chrome_content_browser_client.hchrome/browser/chrome_content_browser_client_unittest.cc
Audit Directions
- Renderer-supplied security flagsAudit browser-side IPC handlers that act on boolean capability flags (
allow_popup-style) from the renderer without independently re-deriving the privilege in the browser. - Renderer-only enforcementReview security decisions enforced only in the renderer (e.g.
ExtensionsRendererClientchecks) that lack a mirrored browser-side check against a compromised process. - Process-privilege verificationCheck that popup/window/navigation privilege checks consult
ProcessMap/ScriptInjectionTracker(or equivalent) to confirm the process truly hosts the claimed extension/hosted-app/content-script context.
Patch
From 3b4c638edf82be4ab208b92263b8c052cd93bdd2 Mon Sep 17 00:00:00 2001 From: Eva Su <[email protected]> Date: Tue, 24 Mar 2026 22:21:13 -0700 Subject: [PATCH] [Extensions] Add browser-side validation to prevent popup blocker bypass Previously, RenderFrameHostImpl::CreateNewWindow() directly trusted the renderer-supplied allow_popup boolean field within the IPC CreateNewWindowParams structure to bypass the popup blocker. A compromised renderer could synthesize IPCs with allow_popup set to true to circumvent this limitation. To prevent this, this CL duplicates the relevant extension checks from the renderer to the browser process. It ensures that the popup bypass is exclusively permitted when the originating process genuinely belongs to an extension, a hosted app, or has had an extension content script injected into it. Specifically, this CL: - Adds ContentBrowserClient::IsPopupBypassAllowed() to allow browser-side verification of the popup bypass capability. - Updates RenderFrameHostImpl::CreateNewWindow() to check this new method alongside the allow_popup IPC flag. - Implements ChromeContentBrowserClient::IsPopupBypassAllowed() to validate that the process either hosts an extension context, a privileged web page (e.g., hosted apps) in an outermost main frame, or has previously run an injected content script. - Strengthens the existing renderer-side checks in ExtensionsRendererClient::AllowPopup() by verifying that the context's extension object corresponds to an actual extension or hosted app for kPrivilegedExtension and kPrivilegedWebPage, respectively. - Adds these histograms to log metrics: - `Extensions.PopupBypassAllowedType`: Logs which extension-related context (Content Script, Extension Process, or Privileged Web Page) successfully bypassed the blocker. This data will help determine if these privileges can be further restricted or removed in the future. - `Extensions.PopupBypassDeniedByBrowser`: Records whenever a renderer-requested bypass (via the `allow_popup` IPC flag) is rejected by the browser-side verification logic. Bug: 489023922 Change-Id: I7cc40c30417ca197c86a46c04bdfed831b173b57 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7669581 Reviewed-by: Elly FJ <[email protected]> Reviewed-by: Alex Moshchuk <[email protected]> Reviewed-by: Devlin Cronin <[email protected]> Commit-Queue: Eva Su <[email protected]> Reviewed-by: Yifan Luo <[email protected]> Cr-Commit-Position: refs/heads/main@{#1604600} --- diff --git a/chrome/browser/chrome_content_browser_client.cc b/chrome/browser/chrome_content_browser_client.cc index cc29df8..797648f 100644 --- a/chrome/browser/chrome_content_browser_client.cc +++ b/chrome/browser/chrome_content_browser_client.cc @@ -632,6 +632,7 @@ #include "extensions/common/extension.h" #include "extensions/common/extension_set.h" #include "extensions/common/manifest_handlers/background_info.h" +#include "extensions/common/mojom/context_type.mojom.h" #include "extensions/common/permissions/permissions_data.h" #include "extensions/common/switches.h" @@ -4434,6 +4435,59 @@ ->GetFeatureObserverClient(); } +// These values are persisted to logs and used for histograms. +enum class PopupBypassType { + kContentScript = 0, + kExtensionProcess = 1, + kPrivilegedWebPage = 2, + kMaxValue = kPrivilegedWebPage, +}; + +bool ChromeContentBrowserClient::IsPopupBypassAllowed( + content::RenderFrameHost* render_frame_host) { +#if BUILDFLAG(ENABLE_EXTENSIONS_CORE) + content::RenderProcessHost* process = render_frame_host->GetProcess(); + content::BrowserContext* browser_context = process->GetBrowserContext(); + extensions::ProcessMap* process_map = + extensions::ProcessMap::Get(browser_context); + if (!process_map) { + return false; + } + + // Allow if it is an authorized extension process. + const extensions::Extension* extension = + process_map->GetEnabledExtensionByProcessID(process->GetID().value()); + if (process_map->CanProcessHostContextType( + extension, *process, + extensions::mojom::ContextType::kPrivilegedExtension)) { + base::UmaHistogramEnumeration("Security.PopupBypassAllowedType", + PopupBypassType::kExtensionProcess); + return true; + } + + // Allow if it is a privileged web page (e.g., hosted app) in an outermost + // main frame. + if (!render_frame_host->GetParentOrOuterDocument() && + process_map->CanProcessHostContextType( + extension, *process, + extensions::mojom::ContextType::kPrivilegedWebPage)) { + base::UmaHistogramEnumeration("Security.PopupBypassAllowedType", + PopupBypassType::kPrivilegedWebPage); + return true; + } + + // Allow if an extension ran a content script in this process. + if (!extensions::ScriptInjectionTracker:: + GetExtensionsThatRanContentScriptsInProcess(*process) + .empty()) { + base::UmaHistogramEnumeration("Security.PopupBypassAllowedType", + PopupBypassType::kContentScript); + return true; + } +#endif + return false; +} + bool ChromeContentBrowserClient::CanCreateWindow( RenderFrameHost* opener, const GURL& opener_url, diff --git a/chrome/browser/chrome_content_browser_client.h b/chrome/browser/chrome_content_browser_client.h index ebd5dfd..a910642 100644 --- a/chrome/browser/chrome_content_browser_client.h +++ b/chrome/browser/chrome_content_browser_client.h @@ -512,6 +512,8 @@ std::unique_ptr<content::ClientCertificateDelegate> delegate) override; content::MediaObserver* GetMediaObserver() override; content::FeatureObserverClient* GetFeatureObserverClient() override; + bool IsPopupBypassAllowed( + content::RenderFrameHost* render_frame_host) override; bool CanCreateWindow(content::RenderFrameHost* opener, const GURL& opener_url, const GURL& opener_top_level_frame_url, diff --git a/chrome/browser/chrome_content_browser_client_unittest.cc b/chrome/browser/chrome_content_browser_client_unittest.cc index d2849ea..272a9390e 100644 --- a/chrome/browser/chrome_content_browser_client_unittest.cc +++ b/chrome/browser/chrome_content_browser_client_unittest.cc @@ -173,6 +173,15 @@ #include "third_party/blink/public/common/features.h" #endif // BUILDFLAG(ENABLE_EXTENSIONS) +#if BUILDFLAG(ENABLE_EXTENSIONS_CORE) +#include "chrome/test/base/chrome_render_view_host_test_harness.h" +#include "extensions/browser/extension_registry.h" +#include "extensions/browser/process_map.h" +#include "extensions/browser/script_injection_tracker.h" +#include "extensions/common/extension_builder.h" +#include "extensions/common/mojom/context_type.mojom.h" +#endif // BUILDFLAG(ENABLE_EXTENSIONS_CORE) + #if BUILDFLAG(ENABLE_PDF) #include "content/public/test/mock_navigation_handle.h" #include "content/public/test/test_renderer_host.h" @@ -222,6 +231,78 @@ TestingProfile profile_; }; +#if BUILDFLAG(ENABLE_EXTENSIONS_CORE) +using ChromeContentBrowserClientIsPopupBypassAllowedTest = + ChromeRenderViewHostTestHarness; + +// Tests that an extension process is allowed to bypass the popup blocker. +TEST_F(ChromeContentBrowserClientIsPopupBypassAllowedTest, ExtensionProcess) { + ChromeContentBrowserClient client; + NavigateAndCommit(GURL("https://example.com")); + EXPECT_FALSE(client.IsPopupBypassAllowed(main_rfh())); + + auto* process_map = extensions::ProcessMap::Get(profile()); + ASSERT_TRUE(process_map); + + scoped_refptr<const extensions::Extension> extension = + extensions::ExtensionBuilder("Test").Build(); + process_map->Insert(extension->id(), + main_rfh()->GetProcess()->GetID().value()); + extensions::ExtensionRegistry::Get(profile())->AddEnabled(extension); + + EXPECT_TRUE(client.IsPopupBypassAllowed(main_rfh())); +} + +// Tests that a privileged hosted app is allowed to bypass the popup blocker. +TEST_F(ChromeContentBrowserClientIsPopupBypassAllowedTest, PrivilegedWebPage) { + ChromeContentBrowserClient client; + NavigateAndCommit(GURL("https://example.com")); + EXPECT_FALSE(client.IsPopupBypassAllowed(main_rfh())); + + scoped_refptr<const extensions::Extension> hosted_app = + extensions::ExtensionBuilder("Hosted App") + .SetManifestKey( + "app", base::DictValue().Set("urls", base::ListValue().Append( + "http://example.com/"))) + .Build(); + + extensions::ExtensionRegistry::Get(profile())->AddEnabled(hosted_app); + auto* process_map = extensions::ProcessMap::Get(profile()); + process_map->Insert(hosted_app->id(), + main_rfh()->GetProcess()->GetID().value()); + + EXPECT_TRUE(client.IsPopupBypassAllowed(main_rfh())); +} + +// Tests that a process where an extension ran a content script is allowed to +// bypass the popup blocker. +TEST_F(ChromeContentBrowserClientIsPopupBypassAllowedTest, ContentScript) { + ChromeContentBrowserClient client; + NavigateAndCommit(GURL("https://example.com")); + EXPECT_FALSE(client.IsPopupBypassAllowed(main_rfh())); + + auto* process_map = extensions::ProcessMap::Get(profile()); + ASSERT_TRUE(process_map); + + scoped_refptr<const extensions::Extension> extension =
Regression Test / PoC
diff --git a/chrome/browser/chrome_content_browser_client_unittest.cc b/chrome/browser/chrome_content_browser_client_unittest.cc
index d2849ea..272a9390e 100644
--- a/chrome/browser/chrome_content_browser_client_unittest.cc
+++ b/chrome/browser/chrome_content_browser_client_unittest.cc
@@ -173,6 +173,15 @@
#include "third_party/blink/public/common/features.h"
#endif // BUILDFLAG(ENABLE_EXTENSIONS)
+#if BUILDFLAG(ENABLE_EXTENSIONS_CORE)
+#include "chrome/test/base/chrome_render_view_host_test_harness.h"
+#include "extensions/browser/extension_registry.h"
+#include "extensions/browser/process_map.h"
+#include "extensions/browser/script_injection_tracker.h"
+#include "extensions/common/extension_builder.h"
+#include "extensions/common/mojom/context_type.mojom.h"
+#endif // BUILDFLAG(ENABLE_EXTENSIONS_CORE)
+
#if BUILDFLAG(ENABLE_PDF)
#include "content/public/test/mock_navigation_handle.h"
#include "content/public/test/test_renderer_host.h"
@@ -222,6 +231,78 @@
TestingProfile profile_;
};
+#if BUILDFLAG(ENABLE_EXTENSIONS_CORE)
+using ChromeContentBrowserClientIsPopupBypassAllowedTest =
+ ChromeRenderViewHostTestHarness;
+
+// Tests that an extension process is allowed to bypass the popup blocker.
+TEST_F(ChromeContentBrowserClientIsPopupBypassAllowedTest, ExtensionProcess) {
+ ChromeContentBrowserClient client;
+ NavigateAndCommit(GURL("https://example.com"));
+ EXPECT_FALSE(client.IsPopupBypassAllowed(main_rfh()));
+
+ auto* process_map = extensions::ProcessMap::Get(profile());
+ ASSERT_TRUE(process_map);
+
+ scoped_refptr<const extensions::Extension> extension =
+ extensions::ExtensionBuilder("Test").Build();
+ process_map->Insert(extension->id(),
+ main_rfh()->GetProcess()->GetID().value());
+ extensions::ExtensionRegistry::Get(profile())->AddEnabled(extension);
+
+ EXPECT_TRUE(client.IsPopupBypassAllowed(main_rfh()));
+}
+
+// Tests that a privileged hosted app is allowed to bypass the popup blocker.
+TEST_F(ChromeContentBrowserClientIsPopupBypassAllowedTest, PrivilegedWebPage) {
+ ChromeContentBrowserClient client;
+ NavigateAndCommit(GURL("https://example.com"));
+ EXPECT_FALSE(client.IsPopupBypassAllowed(main_rfh()));
+
+ scoped_refptr<const extensions::Extension> hosted_app =
+ extensions::ExtensionBuilder("Hosted App")
+ .SetManifestKey(
+ "app", base::DictValue().Set("urls", base::ListValue().Append(
+ "http://example.com/")))
+ .Build();
+
+ extensions::ExtensionRegistry::Get(profile())->AddEnabled(hosted_app);
+ auto* process_map = extensions::ProcessMap::Get(profile());
+ process_map->Insert(hosted_app->id(),
+ main_rfh()->GetProcess()->GetID().value());
+
+ EXPECT_TRUE(client.IsPopupBypassAllowed(main_rfh()));
+}
+
+// Tests that a process where an extension ran a content script is allowed to
+// bypass the popup blocker.
+TEST_F(ChromeContentBrowserClientIsPopupBypassAllowedTest, ContentScript) {
+ ChromeContentBrowserClient client;
+ NavigateAndCommit(GURL("https://example.com"));
+ EXPECT_FALSE(client.IsPopupBypassAllowed(main_rfh()));
+
+ auto* process_map = extensions::ProcessMap::Get(profile());
+ ASSERT_TRUE(process_map);
+
+ scoped_refptr<const extensions::Extension> extension =
+ extensions::ExtensionBuilder("Test").Build();
+
+ extensions::ScriptInjectionTracker::
+ AddExtensionThatRanContentScriptsInProcessForTesting(
+ *main_rfh()->GetProcess(), extension->id());
+
+ EXPECT_TRUE(client.IsPopupBypassAllowed(main_rfh()));
+}
+
+// Tests that a normal web page is not allowed to bypass the popup blocker.
+TEST_F(ChromeContentBrowserClientIsPopupBypassAllowedTest, NormalWebPage) {
+ ChromeContentBrowserClient client;
+ NavigateAndCommit(GURL("https://example.com"));
+ EXPECT_FALSE(client.IsPopupBypassAllowed(main_rfh()));
+}
+
+#endif // BUILDFLAG(ENABLE_EXTENSIONS_CORE)
+
// Check that chrome-native: URLs do not assign a site for their
// SiteInstances. This works because `kChromeNativeScheme` is registered as an
// empty document scheme in ChromeContentClient.
diff --git a/chrome/browser/extensions/window_open_apitest.cc b/chrome/browser/extensions/window_open_apitest.cc
index 1a99f7b..279c057b 100644
--- a/chrome/browser/extensions/window_open_apitest.cc
+++ b/chrome/browser/extensions/window_open_apitest.cc
@@ -223,17 +223,42 @@
// Tests that an extension page can call window.open to an extension URL and
// the new window has extension privileges.
IN_PROC_BROWSER_TEST_F(ExtensionBrowserTest, WindowOpenExtension) {
- ASSERT_TRUE(LoadExtension(
- test_data_dir_.AppendASCII("uitest").AppendASCII("window_open")));
+ const extensions::Extension* extension = LoadExtension(
+ test_data_dir_.AppendASCII("uitest").AppendASCII("window_open"));
+ ASSERT_TRUE(extension);
- GURL start_url(std::string(extensions::kExtensionScheme) +
- url::kStandardSchemeSeparator +
- last_loaded_extension_id() + "/test.html");
+ GURL start_url = extension->GetResourceURL("test.html");
auto* web_contents = GetActiveWebContents();
ASSERT_TRUE(NavigateToURL(web_contents, start_url));
WebContents* newtab = nullptr;
- ASSERT_NO_FATAL_FAILURE(OpenWindow(
- web_contents, start_url.Resolve("newtab.html"), true, true, &newtab));
+ ASSERT_NO_FATAL_FAILURE(OpenWindow(web_contents,
+ extension->GetResourceURL("newtab.html"),
+ true, true, &newtab));
+
+ EXPECT_EQ(true, content::EvalJs(newtab, "testExtensionApi()"));
+}
+
+// Tests that an extension page can call window.open without a user gesture
+// and the new window is opened (bypassing the popup blocker) with extension
+// privileges.
+IN_PROC_BROWSER_TEST_F(ExtensionBrowserTest,
+ WindowOpenExtensionWithoutUserGesture) {
+ const extensions::Extension* extension = LoadExtension(
+ test_data_dir_.AppendASCII("uitest").AppendASCII("window_open"));
+ ASSERT_TRUE(extension);
+
+ GURL start_url = extension->GetResourceURL("test.html");
+ auto* web_contents = GetActiveWebContents();
+ ASSERT_TRUE(NavigateToURL(web_contents, start_url));
+
+ content::WebContentsAddedObserver tab_added_observer;
+ ASSERT_TRUE(content::ExecJs(
+ web_contents,
+ "window.open('" + extension->GetResourceURL("newtab.html").spec() + "');",
+ content::EXECUTE_SCRIPT_NO_USER_GESTURE));
+ content::WebContents* newtab = tab_added_observer.GetWebContents();
+ ASSERT_TRUE(newtab);
+ EXPECT_TRUE(content::WaitForLoadStop(newtab));
EXPECT_EQ(true, content::EvalJs(newtab, "testExtensionApi()"));
}
diff --git a/chrome/browser/ui/blocked_content/popup_blocker_browsertest.cc b/chrome/browser/ui/blocked_content/popup_blocker_browsertest.cc
index 0a51be4..175da29 100644
--- a/chrome/browser/ui/blocked_content/popup_blocker_browsertest.cc
+++ b/chrome/browser/ui/blocked_content/popup_blocker_browsertest.cc
@@ -442,6 +442,27 @@
EXPECT_EQ(base::ASCIIToUTF16(kSearchString), match.contents);
}
+// Verify that the browser process prevents a non-extension process from
+// bypassing the popup blocker. This acts as a browser-side validation against a
+// compromised renderer.
+IN_PROC_BROWSER_TEST_F(PopupBlockerBrowserTest,
+ PopupBypassFromNonExtensionProcessIsBlocked) {
+ GURL url(
+ embedded_test_server()->GetURL("/popup_blocker/popup-window-open.html"));
+ EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
+
+ WebContents* tab = browser()->tab_strip_model()->GetActiveWebContents();
+ content::RenderFrameHost* rfh = tab->GetPrimaryMainFrame();
+
+ // Simulate a compromised renderer trying to bypass the popup blocker.
+ // The popup should be blocked because the renderer is not an extension.
+ EXPECT_TRUE(content::PwnMessageHelper::OpenPopup(rfh, url));
+
+ // The popup should be blocked because the renderer is not an extension.
+ EXPECT_EQ(1u, chrome::GetBrowserCount(browser()->profile()));
+ EXPECT_EQ(1, browser()->tab_strip_model()->count());
+}
+
// This test fails on linux AURA with this change
// https://codereview.chromium.org/23903056
// BUG=https://code.google.com/p/chromium/issues/detail?id=295299
diff --git a/content/public/test/browser_test_utils.cc b/content/public/test/browser_test_utils.cc
index 7979638..e921a8e 100644
--- a/content/public/test/browser_test_utils.cc
+++ b/content/public/test/browser_test_utils.cc
@@ -4184,6 +4184,31 @@
waiter.WaitForOperationToFinish();
}
+bool PwnMessageHelper::OpenPopup(RenderFrameHost* render_frame_host,
+ const GURL& url) {
+ mojom::CreateNewWindowParamsPtr params = mojom::CreateNewWindowParams::New();
+ params->target_url = url;
+ params->allow_popup =
+ true; // The compromised renderer lies and sets this to true
+ params->window_container_type = mojom::WindowContainerType::NORMAL;
+ params->disposition = WindowOpenDisposition::NEW_POPUP;
+ params->features = blink::mojom::WindowFeatures::New();
+ params->referrer = blink::mojom::Referrer::New();
+
+ bool is_blocked = false;
+ base::RunLoop run_loop;
+ static_cast<mojom::FrameHost*>(
+ static_cast<RenderFrameHostImpl*>(render_frame_host))
+ ->CreateNewWindow(
+ std::move(params),
+ base::BindLambdaForTesting([&](mojom::CreateNewWindowStatus status,
+ mojom::CreateNewWindowReplyPtr reply) {
+ is_blocked = (status == mojom::CreateNewWindowStatus::kBlocked);
+ run_loop.Quit();
+ }));
+ run_loop.Run();
+ return is_blocked;
+}
void PwnMessageHelper::OpenURL(RenderFrameHost* render_frame_host,
const GURL& url) {
auto params = blink::mojom::OpenURLParams::New();
diff --git a/content/public/test/browser_test_utils.h b/content/public/test/browser_test_utils.h
index 389aba85..b752e17 100644
--- a/content/public/test/browser_test_utils.h
+++ b/content/public/test/browser_test_utils.h
@@ -2279,6 +2279,8 @@
// Calls OpenURL method in FrameHost Mojo interface.
static void OpenURL(RenderFrameHost* render_frame_host, const GURL& url);
+ static bool OpenPopup(RenderFrameHost* render_frame_host, const GURL& url);
+
private:
PwnMessageHelper(); // Not instantiable.
};
Original Bug Report
A compromised renderer process can circumvent the popup blocker
When the renderer calls the synchronous content.mojom.FrameHost.CreateNewWindow IPC, it fills a CreateNewWindowParams structure that includes a boolean allow_popup field. The browser process reads this field in RenderFrameHostImpl::CreateNewWindow() and uses it to decide whether to proceed with window creation or reject it as a blocked popup.
The critical point is that the browser directly trusts this renderer-supplied value without performing any independent verification of whether a real user gesture actually occurred. If the renderer process has been compromised, it can synthesize IPC’s with allow_popup set to true.
This was split out from:
https://issues.chromium.org/487338366
Note that the general issue around user activation state belonging in the browser process was raised previously in: