Low watch CVSS 5.4 chrome Input Validation 📄 Reporter bug report 🔧 Commit mapped

Overview

Low
Severity
5.4
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Dialog
DescriptionInsufficient validation of untrusted input in Dialog
ComponentDialog
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.
Tracker491676472
Fix commit7e2576672739 (chromium/src) +59/-0
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:L
CISA KEVNot listed
CreditedTianyi Hu
Reported2026-03-11 (per vendor advisory)
Fix landed2026-03-12 (public git)
Disclosed2026-05-05
Patch gap54 days — fix public in source before the advisory

Background

Sandboxed iframe
An <iframe sandbox> whose capabilities are restricted by sandbox flags; allow-modals is the token that permits modal dialogs, and its absence sets the kModals sandbox flag.
`network::mojom::WebSandboxFlags::kModals`
The sandbox flag bit indicating that modal dialogs (alert/confirm/prompt) are disallowed for the frame.
`IsSandboxed(flag)`
A RenderFrameHostImpl query returning whether a given sandbox flag is active on the frame, per browser-tracked state.
`bad_message::ReceivedBadMessage`
The browser-side routine that terminates a renderer process which sent an IPC violating a security invariant.

Root Cause Analysis

The browser-side handler that shows JavaScript modal dialogs — identified by the commit message as RunJavaScriptDialog (its enclosing name is not shown in the @@ hunk header, which exposes only the tail of its parameter list: dialog_type, disable_third_party_subframe_suppresion, ipc_response_callback) — never checked the kModals sandbox flag before proceeding to display the dialog. The allow-modals restriction was enforced only in the renderer (LocalDOMWindow::alert/confirm/prompt), so a compromised renderer could issue the modal-dialog IPC directly and bypass it, violating the invariant that a sandboxed frame lacking allow-modals cannot show modal dialogs.

The fix adds, at the very top of the handler, a check: if IsSandboxed(network::mojom::WebSandboxFlags::kModals) is true, it calls bad_message::ReceivedBadMessage(GetProcess(), bad_message::RFH_MODAL_DIALOG_FROM_SANDBOXED_FRAME) and returns before any dialog logic runs. This mirrors the existing browser-side allow-popups enforcement in CreateNewWindow and makes the restriction authoritative in the browser.

Key insight
The allow-modals sandbox restriction was enforced only renderer-side; the fix adds the matching browser-side IsSandboxed(kModals) guard that terminates a renderer attempting to show a modal dialog from a frame that lacks the capability.

Attack Path

  1. Create a sandboxed frame A page embeds a cross-site iframe with sandbox='allow-scripts' (no allow-modals), as the regression test does with a b.com child under an isolated origin.
  2. Compromise the frame's renderer The attacker gains arbitrary-IPC capability in the sandboxed frame’s renderer process.
  3. Send the dialog IPC directly The renderer invokes the modal-dialog path (e.g. RunModalAlertDialog) over Mojo, skipping the renderer-side LocalDOMWindow::alert check.
  4. Pre-patch: dialog shown Without the browser-side IsSandboxed(kModals) check, the browser proceeds to display a modal dialog attributed to a sandboxed, untrusted frame; post-patch the renderer is instead killed with RFH_MODAL_DIALOG_FROM_SANDBOXED_FRAME.

Impact Assessment

A compromised renderer hosting a sandboxed frame without allow-modals could force the browser to display modal alert/confirm/prompt dialogs, defeating a sandbox restriction and enabling dialog-based UI nuisance or spoofing from untrusted content. The precondition is a compromised renderer able to send the modal-dialog IPC. The vendor advisory rates this low severity and describes it as insufficient validation of untrusted input in Dialog; no exploitation-in-the-wild is stated.

Files Changed

  • content/browser/bad_message.h
  • content/browser/renderer_host/render_frame_host_impl.cc
  • content/browser/renderer_host/render_frame_host_impl.h
  • content/browser/security_exploit_browsertest.cc
  • tools/metrics/histograms/metadata/stability/enums.xml

Audit Directions

  • Renderer-only sandbox enforcement
    For each sandbox flag (kModals, kPopups, kForms, etc.), confirm a browser-side IsSandboxed() check backs the renderer-side gate on any IPC that performs the restricted action.
  • Dialog and UI-surface IPCs
    Audit browser handlers that raise modal or user-visible surfaces for a frame-capability check before display, since these are reachable from a compromised renderer.
  • Parallel-capability patterns
    When one capability (e.g. allow-popups in CreateNewWindow) gains browser-side validation, search for sibling capabilities whose handlers were not similarly updated.
From 7e257667273927316e93b112d1ec27830fdcdffe Mon Sep 17 00:00:00 2001
From: Tianyi Hu <[email protected]>
Date: Wed, 11 Mar 2026 18:16:21 -0700
Subject: [PATCH] Add browser-side validation for allow-modals sandbox attribute

RunJavaScriptDialog() does not check IsSandboxed(kModals) before
showing alert/confirm/prompt dialogs. Add a browser-side check
matching the allow-popups validation in CreateNewWindow(), so that
a sandboxed frame without allow-modals cannot show modal dialogs
even if the renderer is compromised.

Bug: 491676472
Change-Id: I8c3d3c0d2f167754399a67d6970313c3a6acbe64
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7656345
Reviewed-by: Alex Moshchuk <[email protected]>
Reviewed-by: Mark Pearson <[email protected]>
Reviewed-by: Avi Drissman <[email protected]>
Commit-Queue: Alex Moshchuk <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1598123}
---

diff --git a/content/browser/bad_message.h b/content/browser/bad_message.h
index 372417d..54e2b1c 100644
--- a/content/browser/bad_message.h
+++ b/content/browser/bad_message.h
@@ -361,6 +361,7 @@
   RFH_NEW_ISOLATED_WEB_APP_PERMISSION_POLICIES = 333,
   RFH_CREATE_NEW_WINDOW_INVALID_DISPOSITION = 334,
   RFH_CREATE_NEW_WINDOW_FROM_SANDBOXED_FRAME = 335,
+  RFH_MODAL_DIALOG_FROM_SANDBOXED_FRAME = 336,
 
   // 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 c5ee522e..12304d4 100644
--- a/content/browser/renderer_host/render_frame_host_impl.cc
+++ b/content/browser/renderer_host/render_frame_host_impl.cc
@@ -7244,6 +7244,16 @@
     JavaScriptDialogType dialog_type,
     bool disable_third_party_subframe_suppresion,
     JavaScriptDialogCallback ipc_response_callback) {
+  // Sandboxed frames should only be allowed to show modal dialogs when they
+  // have the "allow-modals" attribute. This should have already been checked
+  // by the renderer process (see LocalDOMWindow::alert/confirm/prompt), and
+  // this browser-side check defends against compromised renderers.
+  if (IsSandboxed(network::mojom::WebSandboxFlags::kModals)) {
+    bad_message::ReceivedBadMessage(
+        GetProcess(), bad_message::RFH_MODAL_DIALOG_FROM_SANDBOXED_FRAME);
+    return;
+  }
+
   // Don't show the dialog if it's triggered on a non-active RenderFrameHost
   // or is contained in a Fenced Frame.
   if (!IsActive() || IsNestedWithinFencedFrame()) {
diff --git a/content/browser/renderer_host/render_frame_host_impl.h b/content/browser/renderer_host/render_frame_host_impl.h
index 5664e61..7a1bdc10 100644
--- a/content/browser/renderer_host/render_frame_host_impl.h
+++ b/content/browser/renderer_host/render_frame_host_impl.h
@@ -3493,6 +3493,8 @@
                            CreateNewWindowWithInaccessibleFile);
   FRIEND_TEST_ALL_PREFIXES(SecurityExploitBrowserTest,
                            WindowOpenDisallowedFromSandboxedFrame);
+  FRIEND_TEST_ALL_PREFIXES(SecurityExploitBrowserTest,
+                           ModalDialogDisallowedFromSandboxedFrame);
   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 ccf0287..d2fea7e 100644
--- a/content/browser/security_exploit_browsertest.cc
+++ b/content/browser/security_exploit_browsertest.cc
@@ -1160,6 +1160,51 @@
   EXPECT_EQ(1u, Shell::windows().size());
 }
 
+// Regression test for browser-side validation of the allow-modals sandbox
+// attribute. A sandboxed frame without allow-modals should not be able to show
+// modal dialogs (alert, confirm, prompt). This is a variant of
+// WindowOpenDisallowedFromSandboxedFrame for the kModals sandbox flag.
+IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
+                       ModalDialogDisallowedFromSandboxedFrame) {
+  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-modals
+  // attribute, so it should not be allowed to show modal dialogs.
+  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::kModals));
+
+  // Simulate that the b.com renderer is compromised and sends an IPC to show
+  // a modal dialog, bypassing renderer-side checks in LocalDOMWindow::alert().
+  // The browser process should detect this and terminate the renderer.
+  RenderProcessHostBadIpcMessageWaiter kill_waiter(subframe->GetProcess());
+  subframe->RunModalAlertDialog(u"test", false, base::DoNothing());
+  EXPECT_EQ(bad_message::RFH_MODAL_DIALOG_FROM_SANDBOXED_FRAME,
+            kill_waiter.Wait());
+  EXPECT_FALSE(subframe->IsRenderFrameLive());
+}
+
 // Regression test for browser-side validation of POST submissions in
 // CreateNewWindow. See https://crbug.com/487768779.
 IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
diff --git a/tools/metrics/histograms/metadata/stability/enums.xml b/tools/metrics/histograms/metadata/stability/enums.xml
index de97da0..3ea66989 100644
--- a/tools/metrics/histograms/metadata/stability/enums.xml
+++ b/tools/metrics/histograms/metadata/stability/enums.xml
@@ -501,6 +501,7 @@
   <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"/>
+  <int value="336" label="RFH_MODAL_DIALOG_FROM_SANDBOXED_FRAME"/>
 </enum>
 
 <enum name="BadMessageReasonExtensions">
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/security_exploit_browsertest.cc b/content/browser/security_exploit_browsertest.cc
index ccf0287..d2fea7e 100644
--- a/content/browser/security_exploit_browsertest.cc
+++ b/content/browser/security_exploit_browsertest.cc
@@ -1160,6 +1160,51 @@
   EXPECT_EQ(1u, Shell::windows().size());
 }
 
+// Regression test for browser-side validation of the allow-modals sandbox
+// attribute. A sandboxed frame without allow-modals should not be able to show
+// modal dialogs (alert, confirm, prompt). This is a variant of
+// WindowOpenDisallowedFromSandboxedFrame for the kModals sandbox flag.
+IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
+                       ModalDialogDisallowedFromSandboxedFrame) {
+  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-modals
+  // attribute, so it should not be allowed to show modal dialogs.
+  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::kModals));
+
+  // Simulate that the b.com renderer is compromised and sends an IPC to show
+  // a modal dialog, bypassing renderer-side checks in LocalDOMWindow::alert().
+  // The browser process should detect this and terminate the renderer.
+  RenderProcessHostBadIpcMessageWaiter kill_waiter(subframe->GetProcess());
+  subframe->RunModalAlertDialog(u"test", false, base::DoNothing());
+  EXPECT_EQ(bad_message::RFH_MODAL_DIALOG_FROM_SANDBOXED_FRAME,
+            kill_waiter.Wait());
+  EXPECT_FALSE(subframe->IsRenderFrameLive());
+}
+
 // Regression test for browser-side validation of POST submissions in
 // CreateNewWindow. See https://crbug.com/487768779.
 IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
Loading diff…

Original Bug Report

reported by [email protected]

RenderFrameHostImpl::RunJavaScriptDialog() missing browser-side allow-modals sandbox flag check


Report description

RenderFrameHostImpl::RunJavaScriptDialog() missing browser-side allow-modals sandbox flag check


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:content/browser/renderer_host/render_frame_host_impl.cc


The problem

Please describe the technical details of the vulnerability

This is a variant of issue 487471101 (allow-popups sandbox bypass). I am filing it separately since it affects a different IPC entrypoint (RunJavaScriptDialog vs CreateNewWindow) and requires its own fix, but I understand it may be considered part of the same effort tracked in issue 40607568. I leave it to the security team to decide whether this warrants a separate report or should be folded into the existing tracking bug.

RunJavaScriptDialog() does not check IsSandboxed(kModals) before showing alert(), confirm(), or prompt() dialogs. A compromised renderer in a sandboxed iframe (without allow-modals) can send RunModalAlertDialog/RunModalConfirmDialog/RunModalPromptDialog IPC directly and the browser shows the dialogs.

Vulnerable file: render_frame_host_impl.cc - RunJavaScriptDialog()

The renderer-side check in LocalDOMWindow::alert()/confirm()/prompt() blocks sandboxed calls, but the browser entrypoint has no corresponding check. Compare with the kPopups fix in CreateNewWindow() which checks IsSandboxed(kPopups) + ReceivedBadMessage.

Steps to reproduce:

  1. Check out stable tag: git checkout 146.0.7680.32
  2. git apply poc_patch.diff (removes renderer-side kModals checks to simulate compromised renderer)
  3. autoninja -C out/Default chrome
  4. Start HTTP server: python3 serve.py
  5. Launch patched Chrome:
    • out/Default/Chromium.app/Contents/MacOS/Chromium --user-data-dir=/tmp/md1 http://127.0.0.1:8080/index.html
    • Observe: alert(), confirm(), and prompt() dialogs appear from <iframe sandbox="allow-scripts"> (no allow-modals). The iframe has opaque origin (null).

Bisect:

  • Introducing commit: 15bee8d78bc8b - [email protected], May 22 2015. Added allow-modals sandbox flag with renderer-only enforcement; browser-side RunJavaScriptDialog() was never updated.
  • Variant gap widened by a12e651f6f626 (Feb 28, 2026) which added browser-side enforcement for kPopups but not kModals.
  • Affected: M44 through M146 (current stable).

Fix: Add IsSandboxed(kModals) check + ReceivedBadMessage in RunJavaScriptDialog(), matching the kPopups pattern in CreateNewWindow().

if (IsSandboxed(network::mojom::WebSandboxFlags::kModals)) {
  bad_message::ReceivedBadMessage(
      GetProcess(),
      bad_message::RFH_MODAL_DIALOG_FROM_SANDBOXED_FRAME);
  return;
}

Impact analysis

A compromised renderer in a cross-site sandboxed iframe (separate process via site isolation) can show modal dialogs that block the page, phish via prompt(), or manipulate user decisions via confirm(). Same class as bug 487471101 but lower impact since dialogs are less sensitive than popups.


The cause

What version of Chrome have you found the security issue in?

146.0.7680.32 (Stable)

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

View on issue tracker