Medium watch CVSS 5.4 chrome Spoofing 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
5.4
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUI misrepresentation in Navigation
DescriptionUI misrepresentation in Navigation
ComponentNavigation
Bug ClassSpoofing
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.
Tracker514006744
Fix commitab2be7c3ec69 (chromium/src) +113/-3
CWECWE-451
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:L
CISA KEVNot listed
CreditedGoogle
Reported2026-05-17 (per vendor advisory)
Fix landed2026-07-23 (public git)
Disclosed2026-08-25
Patch gap33 days — fix public in source before the advisory

Background

Paint holding
an optimization that keeps the old document’s pixels on screen briefly during a navigation to avoid a flash, which is safe only when the retained content cannot be attacker-controlled relative to the new URL shown in the omnibox.
Initial empty document
the about:blank document a frame has before its first real navigation; FrameTreeNode::is_on_initial_empty_document() reports this state.
Opener-inherited origin
a popup created via window.open() starts on an initial about:blank that inherits the opener’s origin, so the opener can script it (e.g., w.document.body.innerHTML = ...) without clearing the initial-empty-document state.
Opaque origin with no precursor
a unique opaque origin (origin.opaque() and GetTupleOrPrecursorTupleIfOpaque().IsValid() false) that nothing can script, characteristic of a brand-new browser-created tab.

Root Cause Analysis

In Navigator::DidNavigate, main-frame paint holding was allowed whenever was_on_initial_empty_document was true (OR-ed with same-origin, user-activation, and client-override conditions). That exemption was intended only for the first navigation in a fresh browser-created tab, but it also matched opener-created popups whose initial about:blank inherits the opener’s origin and can be filled with attacker markup via DOM APIs while remaining on the initial empty document. Consequently, when the opener navigated such a popup cross-origin without user activation, the opener-written content was held on screen as paint-holding fallback even as the omnibox updated to the new cross-origin URL, enabling a URL spoof.

The fix introduces allow_paint_holding_for_initial_empty_document = was_on_initial_empty_document && old_frame_origin.opaque() && !old_frame_origin.GetTupleOrPrecursorTupleIfOpaque().IsValid() and substitutes it for the bare was_on_initial_empty_document term, so the exemption applies only when the pre-navigation origin is a unique opaque origin with no precursor — i.e., a genuine browser-created tab that no opener could have scripted. This tightens the condition; opener-created popups (non-opaque, or opaque-with-precursor) no longer qualify.

Key insight
The single mistake was gating the paint-holding exemption on was_on_initial_empty_document alone, which also covered scriptable opener-created popups; the fix additionally requires the old origin to be a unique opaque origin with no precursor, restricting the exemption to unscriptable brand-new tabs.

Attack Path

  1. Open and populate a popup An attacker page calls window.open() and writes content into the popup’s initial about:blank (e.g., w.document.body.innerHTML = 'hi'); the popup inherits the opener’s origin and stays on its initial empty document.
  2. Navigate cross-origin without activation The opener sets window.w.location to a cross-origin URL (url_b) with no user gesture, so the navigation begins while is_on_initial_empty_document() is still true.
  3. Exemption misfires Pre-fix, was_on_initial_empty_document alone satisfied allow_main_frame_paint_holding, so paint holding retained the opener-written content during the cross-origin navigation.
  4. Spoof the URL The omnibox updates to the new cross-origin URL while the attacker-controlled fallback pixels remain displayed, presenting attacker content as though it came from the navigated origin.

Impact Assessment

An attacker-controlled opener can display arbitrary content it wrote into a popup while the omnibox shows a different, cross-origin URL, producing a URL-spoofing / UI-misrepresentation condition in the browser’s navigation UI. The preconditions are an opener-created popup still on its initial empty document navigated cross-origin without user activation, as demonstrated by the two added browser tests (non-opaque and opaque-with-precursor openers). The advisory classifies this as spoofing (UI misrepresentation in Navigation) and does not report in-the-wild exploitation; no escalation beyond the spoof is demonstrated.

Changed Functions

FunctionChangeNotes
RenderFrameHostImplBrowserTestWithBFCacheAndViewTransition
content/browser/renderer_host/render_frame_host_impl_browsertest.cc
modified

Files Changed

  • content/browser/renderer_host/navigator.cc
  • content/browser/renderer_host/render_frame_host_impl_browsertest.cc

Audit Directions

  • Initial-empty-document assumptions
    Audit code that treats is_on_initial_empty_document() as implying “brand-new, uncontrolled” state, since opener-created popups inherit the opener’s origin and are scriptable while still on the initial empty document.
  • Origin-based safety gates
    Look for security decisions keyed on a frame state flag that should instead (or also) check whether the pre-navigation origin is a unique opaque origin with no precursor, i.e., truly unscriptable.
  • Cross-origin UI retention
    Review other places that retain old-document pixels or UI during navigation (paint holding, transitions, fallback content) for cross-origin, non-activated navigations that could desynchronize displayed content from the omnibox URL.
From ab2be7c3ec69432cff595bc629ccf14f294475e9 Mon Sep 17 00:00:00 2001
From: Alex Moshchuk <[email protected]>
Date: Thu, 23 Jul 2026 14:01:10 -0700
Subject: [PATCH] Restrict initial-empty-document paint-holding exemption to browser-created tabs

Navigator::DidNavigate() allowed main-frame paint holding for any
navigation from an initial empty document. The exemption was added to
keep paint holding for the first navigation in a fresh browser-created
tab (crbug.com/367623929, seemingly for test purposes), but it also
applied to popups created via window.open(). An opener-created popup's
initial about:blank inherits the opener's origin and can be modified by
that opener via DOM APIs without clearing
is_on_initial_empty_document(). This would allow the opener to spoof a
URL if it navigated the popup cross-origin.

This CL tightens the initial empty doc exemption to browser-created tabs
which have unique opaque origins, with no precursors (rather than
inheriting an origin from an opener). Unique opaque origins can't be
scripted by anything, so paint holding remains safe for those cases. The
CL adds two tests (mostly Gemini-written) for openers with non-opaque
and opaque origins, which fail without the fix.

Bug: 514006744
Change-Id: Idd36cca191a0249e1130d68638866c99dd393eda
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8135131
Reviewed-by: Vladimir Levin <[email protected]>
Commit-Queue: Alex Moshchuk <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1667390}
---
diff --git a/content/browser/renderer_host/navigator.cc b/content/browser/renderer_host/navigator.cc
index 6fb8891..b7bfdf2 100644
--- a/content/browser/renderer_host/navigator.cc
+++ b/content/browser/renderer_host/navigator.cc
@@ -563,8 +563,9 @@
   // Allow main frame paint holding in the following cases:
   //  - We don't have an animated transition. See crbug.com/360844863.
   //  - At least one of the following conditions is true:
-  //    - This is a navigation from the initial document. This part helps with
-  //      tests. See crbug.com/367623929.
+  //    - This is a navigation from the initial document (in cases where this is
+  //      a brand new tab that didn't inherit another origin from an opener).
+  //      This part helps with tests. See crbug.com/367623929.
   //    - This is a same origin navigation (or we're not limiting cross-origin
   //      paint holding)
   //    - There is a user activation. This means that the user interacted with
@@ -577,9 +578,12 @@
   // See https://issues.chromium.org/40942531 for reasons we limit paint
   // holding.
   ContentBrowserClient* client = GetContentClient()->browser();
+  const bool allow_paint_holding_for_initial_empty_document =
+      was_on_initial_empty_document && old_frame_origin.opaque() &&
+      !old_frame_origin.GetTupleOrPrecursorTupleIfOpaque().IsValid();
   const bool allow_main_frame_paint_holding =
       !navigation_request->was_initiated_by_animated_transition() &&
-      (was_on_initial_empty_document ||
+      (allow_paint_holding_for_initial_empty_document ||
        old_frame_origin.IsSameOriginWith(params.origin) ||
        old_frame_host->HasStickyUserActivation() ||
        client->AllowNonActivatedCrossOriginPaintHolding() ||
diff --git a/content/browser/renderer_host/render_frame_host_impl_browsertest.cc b/content/browser/renderer_host/render_frame_host_impl_browsertest.cc
index 62c2f2a8..120af00 100644
--- a/content/browser/renderer_host/render_frame_host_impl_browsertest.cc
+++ b/content/browser/renderer_host/render_frame_host_impl_browsertest.cc
@@ -9705,6 +9705,112 @@
   EXPECT_FALSE(rwhi_a->IsContentRenderingTimeoutRunning());
 }
 
+// Tests that paint holding is not used when an opener-created popup that is
+// still on its initial empty document navigates cross-origin without user
+// activation. The popup's initial empty document inherits the opener's origin,
+// so the opener can write content into it; that content should not remain on
+// screen as paint-holding fallback once the omnibox shows the new cross-origin
+// URL.
+IN_PROC_BROWSER_TEST_F(
+    RenderFrameHostImplBrowserTest,
+    NoPaintHoldingForCrossOriginNavigationFromOpenerCreatedInitialDocument) {
+  GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
+  GURL url_b(embedded_test_server()->GetURL("b.com", "/title2.html"));
+
+  // Navigate the opener to a.com.
+  EXPECT_TRUE(NavigateToURL(shell(), url_a));
+
+  // Open a popup. Its initial empty document inherits a.com's origin and can
+  // be modified by the opener.
+  ShellAddedObserver new_shell_observer;
+  EXPECT_TRUE(ExecJs(
+      shell(), "window.w = window.open(); w.document.body.innerHTML = 'hi';"));
+  Shell* popup = new_shell_observer.GetShell();
+  WebContentsImpl* popup_contents =
+      static_cast<WebContentsImpl*>(popup->web_contents());
+  EXPECT_TRUE(WaitForLoadStop(popup_contents));
+
+  RenderFrameHostImpl* popup_rfh = popup_contents->GetPrimaryMainFrame();
+  EXPECT_TRUE(popup_rfh->frame_tree_node()->is_on_initial_empty_document());
+  EXPECT_FALSE(popup_rfh->GetLastCommittedOrigin().opaque());
+  EXPECT_FALSE(popup_rfh->HasStickyUserActivation());
+
+  // Navigate the popup cross-site from the opener, as a real page would.
+  TestNavigationObserver nav_observer(popup_contents);
+  EXPECT_TRUE(ExecJs(shell(), JsReplace("window.w.location = $1;", url_b),
+                     EXECUTE_SCRIPT_NO_USER_GESTURE));
+  nav_observer.Wait();
+  EXPECT_TRUE(WaitForLoadStop(popup_contents));
+  EXPECT_EQ(url_b, popup_contents->GetLastCommittedURL());
+
+  // Paint holding should not be used for this cross-origin, non-activated
+  // navigation, even though it started from the initial empty document. The
+  // content rendering timeout is used here as a proxy for paint holding.
+  RenderWidgetHostImpl* popup_rwhi =
+      popup_contents->GetPrimaryMainFrame()->GetRenderWidgetHost();
+  EXPECT_FALSE(popup_rwhi->IsContentRenderingTimeoutRunning());
+}
+
+// Tests that paint holding is not used when an opener-created popup that is
+// still on its initial empty document navigates cross-origin without user
+// activation. Similar to the test above, but checks the case where the opener
+// has an opaque origin.
+IN_PROC_BROWSER_TEST_F(
+    RenderFrameHostImplBrowserTest,
+    NoPaintHoldingForCrossOriginNavigationFromOpaqueOpenerCreatedInitialDocument) {
+  GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
+  GURL url_b(embedded_test_server()->GetURL("b.com", "/title2.html"));
+
+  // Navigate the opener to a.com.
+  EXPECT_TRUE(NavigateToURL(shell(), url_a));
+
+  // Create a data: URL iframe to give the attacker an opaque origin.
+  TestNavigationObserver data_nav_observer(shell()->web_contents());
+  ShellAddedObserver new_shell_observer;
+  EXPECT_TRUE(ExecJs(shell(), R"(
+    let f = document.createElement('iframe');
+    f.src = 'data:text/html,<script>window.w = window.open(); ' +
+            'w.document.body.innerHTML = "hi";</script>';
+    document.body.appendChild(f);
+  )"));
+  data_nav_observer.Wait();
+
+  RenderFrameHostImpl* data_rfh = static_cast<RenderFrameHostImpl*>(
+      ChildFrameAt(shell()->web_contents()->GetPrimaryMainFrame(), 0));
+  EXPECT_TRUE(data_rfh->GetLastCommittedOrigin().opaque());
+
+  // Wait for the popup to be created and load.
+  Shell* popup = new_shell_observer.GetShell();
+  WebContentsImpl* popup_contents =
+      static_cast<WebContentsImpl*>(popup->web_contents());
+  EXPECT_TRUE(WaitForLoadStop(popup_contents));
+
+  RenderFrameHostImpl* popup_rfh = popup_contents->GetPrimaryMainFrame();
+  EXPECT_TRUE(popup_rfh->frame_tree_node()->is_on_initial_empty_document());
+  EXPECT_TRUE(popup_rfh->GetLastCommittedOrigin().opaque());
+  EXPECT_TRUE(data_rfh->GetLastCommittedOrigin()
+                  .GetTupleOrPrecursorTupleIfOpaque()
+                  .IsValid());
+  EXPECT_EQ(
+      data_rfh->GetLastCommittedOrigin().GetTupleOrPrecursorTupleIfOpaque(),
+      popup_rfh->GetLastCommittedOrigin().GetTupleOrPrecursorTupleIfOpaque());
+  EXPECT_FALSE(popup_rfh->HasStickyUserActivation());
+
+  // Navigate the popup cross-site from the opener, as a real page would.
+  TestNavigationObserver nav_observer(popup_contents);
+  EXPECT_TRUE(ExecJs(data_rfh, JsReplace("window.w.location = $1;", url_b),
+                     EXECUTE_SCRIPT_NO_USER_GESTURE));
+  nav_observer.Wait();
+  EXPECT_TRUE(WaitForLoadStop(popup_contents));
+  EXPECT_EQ(url_b, popup_contents->GetLastCommittedURL());
+
+  // Paint holding should not be used for this cross-origin, non-activated
+  // navigation.
+  RenderWidgetHostImpl* popup_rwhi =
+      popup_contents->GetPrimaryMainFrame()->GetRenderWidgetHost();
+  EXPECT_FALSE(popup_rwhi->IsContentRenderingTimeoutRunning());
+}
+
 namespace {
 
 class RenderFrameHostImplBrowserTestWithBFCacheAndViewTransition
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/renderer_host/render_frame_host_impl_browsertest.cc b/content/browser/renderer_host/render_frame_host_impl_browsertest.cc
index 62c2f2a8..120af00 100644
--- a/content/browser/renderer_host/render_frame_host_impl_browsertest.cc
+++ b/content/browser/renderer_host/render_frame_host_impl_browsertest.cc
@@ -9705,6 +9705,112 @@
   EXPECT_FALSE(rwhi_a->IsContentRenderingTimeoutRunning());
 }
 
+// Tests that paint holding is not used when an opener-created popup that is
+// still on its initial empty document navigates cross-origin without user
+// activation. The popup's initial empty document inherits the opener's origin,
+// so the opener can write content into it; that content should not remain on
+// screen as paint-holding fallback once the omnibox shows the new cross-origin
+// URL.
+IN_PROC_BROWSER_TEST_F(
+    RenderFrameHostImplBrowserTest,
+    NoPaintHoldingForCrossOriginNavigationFromOpenerCreatedInitialDocument) {
+  GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
+  GURL url_b(embedded_test_server()->GetURL("b.com", "/title2.html"));
+
+  // Navigate the opener to a.com.
+  EXPECT_TRUE(NavigateToURL(shell(), url_a));
+
+  // Open a popup. Its initial empty document inherits a.com's origin and can
+  // be modified by the opener.
+  ShellAddedObserver new_shell_observer;
+  EXPECT_TRUE(ExecJs(
+      shell(), "window.w = window.open(); w.document.body.innerHTML = 'hi';"));
+  Shell* popup = new_shell_observer.GetShell();
+  WebContentsImpl* popup_contents =
+      static_cast<WebContentsImpl*>(popup->web_contents());
+  EXPECT_TRUE(WaitForLoadStop(popup_contents));
+
+  RenderFrameHostImpl* popup_rfh = popup_contents->GetPrimaryMainFrame();
+  EXPECT_TRUE(popup_rfh->frame_tree_node()->is_on_initial_empty_document());
+  EXPECT_FALSE(popup_rfh->GetLastCommittedOrigin().opaque());
+  EXPECT_FALSE(popup_rfh->HasStickyUserActivation());
+
+  // Navigate the popup cross-site from the opener, as a real page would.
+  TestNavigationObserver nav_observer(popup_contents);
+  EXPECT_TRUE(ExecJs(shell(), JsReplace("window.w.location = $1;", url_b),
+                     EXECUTE_SCRIPT_NO_USER_GESTURE));
+  nav_observer.Wait();
+  EXPECT_TRUE(WaitForLoadStop(popup_contents));
+  EXPECT_EQ(url_b, popup_contents->GetLastCommittedURL());
+
+  // Paint holding should not be used for this cross-origin, non-activated
+  // navigation, even though it started from the initial empty document. The
+  // content rendering timeout is used here as a proxy for paint holding.
+  RenderWidgetHostImpl* popup_rwhi =
+      popup_contents->GetPrimaryMainFrame()->GetRenderWidgetHost();
+  EXPECT_FALSE(popup_rwhi->IsContentRenderingTimeoutRunning());
+}
+
+// Tests that paint holding is not used when an opener-created popup that is
+// still on its initial empty document navigates cross-origin without user
+// activation. Similar to the test above, but checks the case where the opener
+// has an opaque origin.
+IN_PROC_BROWSER_TEST_F(
+    RenderFrameHostImplBrowserTest,
+    NoPaintHoldingForCrossOriginNavigationFromOpaqueOpenerCreatedInitialDocument) {
+  GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
+  GURL url_b(embedded_test_server()->GetURL("b.com", "/title2.html"));
+
+  // Navigate the opener to a.com.
+  EXPECT_TRUE(NavigateToURL(shell(), url_a));
+
+  // Create a data: URL iframe to give the attacker an opaque origin.
+  TestNavigationObserver data_nav_observer(shell()->web_contents());
+  ShellAddedObserver new_shell_observer;
+  EXPECT_TRUE(ExecJs(shell(), R"(
+    let f = document.createElement('iframe');
+    f.src = 'data:text/html,<script>window.w = window.open(); ' +
+            'w.document.body.innerHTML = "hi";</script>';
+    document.body.appendChild(f);
+  )"));
+  data_nav_observer.Wait();
+
+  RenderFrameHostImpl* data_rfh = static_cast<RenderFrameHostImpl*>(
+      ChildFrameAt(shell()->web_contents()->GetPrimaryMainFrame(), 0));
+  EXPECT_TRUE(data_rfh->GetLastCommittedOrigin().opaque());
+
+  // Wait for the popup to be created and load.
+  Shell* popup = new_shell_observer.GetShell();
+  WebContentsImpl* popup_contents =
+      static_cast<WebContentsImpl*>(popup->web_contents());
+  EXPECT_TRUE(WaitForLoadStop(popup_contents));
+
+  RenderFrameHostImpl* popup_rfh = popup_contents->GetPrimaryMainFrame();
+  EXPECT_TRUE(popup_rfh->frame_tree_node()->is_on_initial_empty_document());
+  EXPECT_TRUE(popup_rfh->GetLastCommittedOrigin().opaque());
+  EXPECT_TRUE(data_rfh->GetLastCommittedOrigin()
+                  .GetTupleOrPrecursorTupleIfOpaque()
+                  .IsValid());
+  EXPECT_EQ(
+      data_rfh->GetLastCommittedOrigin().GetTupleOrPrecursorTupleIfOpaque(),
+      popup_rfh->GetLastCommittedOrigin().GetTupleOrPrecursorTupleIfOpaque());
+  EXPECT_FALSE(popup_rfh->HasStickyUserActivation());
+
+  // Navigate the popup cross-site from the opener, as a real page would.
+  TestNavigationObserver nav_observer(popup_contents);
+  EXPECT_TRUE(ExecJs(data_rfh, JsReplace("window.w.location = $1;", url_b),
+                     EXECUTE_SCRIPT_NO_USER_GESTURE));
+  nav_observer.Wait();
+  EXPECT_TRUE(WaitForLoadStop(popup_contents));
+  EXPECT_EQ(url_b, popup_contents->GetLastCommittedURL());
+
+  // Paint holding should not be used for this cross-origin, non-activated
+  // navigation.
+  RenderWidgetHostImpl* popup_rwhi =
+      popup_contents->GetPrimaryMainFrame()->GetRenderWidgetHost();
+  EXPECT_FALSE(popup_rwhi->IsContentRenderingTimeoutRunning());
+}
+
 namespace {
 
 class RenderFrameHostImplBrowserTestWithBFCacheAndViewTransition
Loading diff…

Original Bug Report

reported by [email protected]

URL spoof via paint-holding on modified initial empty document

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A logic error in Chromium’s paint-holding mechanism allows a transient cross-origin URL spoof when an opener modifies a popup’s initial empty document. Attacker-controlled content can remain visible for up to 4 seconds while the omnibox shows a new, committed cross-origin URL. This occurs because DOM mutations on the initial document do not clear the ‘initial empty document’ status used for paint-holding exemptions.

Affected files:

  • content/browser/renderer_host/navigator.cc
  • content/browser/renderer_host/frame_tree_node.cc
  • content/browser/renderer_host/render_frame_host_manager.cc
  • content/browser/renderer_host/navigation_controller_impl.cc
  • content/browser/renderer_host/frame_tree.cc
  • content/browser/renderer_host/render_frame_host_impl.cc
  • content/browser/renderer_host/render_widget_host_impl.cc

Estimated timestamp from git blame: Unknown (Google3 checkout)

Summary

A potential vulnerability in Chromium’s paint-holding mechanism allows for a transient visual URL spoof. When a window is opened via window.open(), the opener can modify the initial about:blank document (e.g., via innerHTML). If the popup is then navigated to a cross-origin URL without user activation, the browser’s paint-holding logic inadvertently allows the attacker-controlled content to remain visible while the omnibox updates to the new, committed URL.

Technical Analysis

The issue stems from how Chromium tracks the state of the initial empty document across two different flags:

  1. FrameTreeNode::is_on_initial_empty_document_: Initialized to true. It is only cleared by document.open() or a non-blank navigation commit.
  2. FrameTree::has_accessed_initial_main_document_: Set to true when the initial document is scripted or modified.

In Navigator::DidNavigate() (content/browser/renderer_host/navigator.cc), the decision to permit paint-holding for a main-frame navigation depends on whether frame_tree_node->is_on_initial_empty_document() was true before the navigation committed. This check is intended to provide smooth transitions for new tabs but fails to account for cases where the ’empty’ document was modified by its opener.

Because direct DOM mutations (like innerHTML) do not clear the is_on_initial_empty_document_ flag, Navigator::DidNavigate grants a paint-holding exemption even if the navigation is cross-origin and lacks user activation. This bypasses the security mitigation kLimitCrossOriginNonActivatedPaintHolding (content/common/features.cc), which is designed to prevent precisely this type of spoofing.

When the navigation commits:

  1. RenderFrameHostManager::CommitPending() captures the modified about:blank surface as ‘fallback content’.
  2. the omnibox updates to the new committed URL.
  3. RenderWidgetHostImpl displays the attacker’s pixels for up to 4 seconds (the kNewContentRenderingDelay timeout) or until the new page renders its first frame.

User input is suppressed during this period via RenderWidgetHostImpl::InitializePaintHolding(true), so the impact is limited to a visual spoof and cannot be used for direct interaction attacks (like clickjacking).

Potential Reproduction Steps

  1. Serve a page on https://attacker.example that opens a popup: const p = window.open();.
  2. Modify the popup content: p.document.body.innerHTML = '<h1>Logged out of bank.example. Re-enter password.</h1>';.
  3. Navigate the popup cross-origin: p.location = 'https://bank.example/login';.
  4. Observe that after the navigation commits, the omnibox shows https://bank.example/login while the content area still shows the attacker’s message.

Suggested Fix

The paint-holding decision in Navigator::DidNavigate() should be updated to ensure that the exemption for the initial empty document is only granted if the document has not been modified. This can be achieved by checking frame_tree.has_accessed_initial_main_document() in addition to is_on_initial_empty_document() when evaluating allow_main_frame_paint_holding.

Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker