Medium watch CVSS 4.2 chrome Input Validation 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
4.2
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Popup Blocker
DescriptionInsufficient validation of untrusted input in Popup Blocker
ComponentPopup Blocker
Sub-componentExtensions
Bug ClassInput Validation
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.
Tracker489023922
Fix commit3b4c638edf82 (chromium/src) +296/-10
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-03-02 (per vendor advisory)
Fix landed2026-03-25 (public git)
Disclosed2026-05-05
Patch gap41 days — fix public in source before the advisory

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.

Key insight
The browser trusted a renderer-controlled 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

  1. Compromise a renderer An attacker gains code execution in a non-privileged (ordinary web) renderer process (inferred precondition stated in the commit message).
  2. Forge the bypass flag The compromised renderer sends a CreateNewWindow IPC with CreateNewWindowParams::allow_popup set to true, which the regression test models with PwnMessageHelper::OpenPopup.
  3. Browser trusts the flag (pre-patch) RenderFrameHostImpl::CreateNewWindow() honors allow_popup without independent verification, so the popup blocker is bypassed from a process that has no legitimate privilege.
  4. Open unsolicited popups The renderer opens windows/popups without a user gesture, defeating the popup blocker (post-patch, IsPopupBypassAllowed returns false for such a process and the bypass is denied).

Impact Assessment

The affected capability is the popup blocker: a compromised renderer could bypass it to open windows without a user gesture, which assists abuse/annoyance and potentially navigation/UI tricks, in the renderer-to-browser trust boundary. The advisory rates this medium severity and classifies it as insufficient validation of untrusted input in Popup Blocker; it does not report in-the-wild exploitation. The renderer-compromise precondition and any further use of the opened window are (inferred); the patch itself demonstrates only the missing browser-side validation.

Changed Functions

FunctionChangeNotes
if
chrome/browser/chrome_content_browser_client.cc
modified
TEST_F
chrome/browser/chrome_content_browser_client_unittest.cc
modified

Files Changed

  • chrome/browser/chrome_content_browser_client.cc
  • chrome/browser/chrome_content_browser_client.h
  • chrome/browser/chrome_content_browser_client_unittest.cc

Audit Directions

  • Renderer-supplied security flags
    Audit 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 enforcement
    Review security decisions enforced only in the renderer (e.g. ExtensionsRendererClient checks) that lack a mirrored browser-side check against a compromised process.
  • Process-privilege verification
    Check 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.
From 3b4c638edf82be4ab208b92263b8c052cd93bdd2 Mon Sep 17 00:00:00 2001
From: Eva Su <evasu@chromium.org>
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 <ellyjones@chromium.org>
Reviewed-by: Alex Moshchuk <alexmos@chromium.org>
Reviewed-by: Devlin Cronin <rdevlin.cronin@chromium.org>
Commit-Queue: Eva Su <evasu@chromium.org>
Reviewed-by: Yifan Luo <lyf@chromium.org>
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 =
Loading diff…

Regression Test / PoC

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

Original Bug Report

reported by sz...@chromium.org

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:

https://issues.chromium.org/40091540

View on issue tracker