Low watch CVSS 4.3 chrome Spoofing 📄 Reporter bug report 🔧 Commit mapped

Overview

Low
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect security UI in PictureInPicture
DescriptionIncorrect security UI in PictureInPicture
ComponentPictureInPicture
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.
Tracker475238879
Fix commitb7b72d7dfb58 (chromium/src) +220/-1
CWECWE-451
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:N
CISA KEVNot listed
CreditedBarath Stalin K
Reported2026-01-12 (per vendor advisory)
Fix landed2026-01-23 (public git)
Disclosed2026-03-10
Patch gap46 days — fix public in source before the advisory

Background

Media session `source_title`
The string MediaSessionImpl builds from the playing page’s URL and exposes as the media’s source. The Picture-in-Picture overlay shows it as the origin (origin_for_testing() in the test) so the user knows which site owns the floating window.
Opaque origin
A url::Origin with no scheme/host/port tuple, as given to CSP-sandboxed documents and some about:blank popups. origin.GetURL() on an opaque origin produces no meaningful host to show.
Precursor origin
The tuple an opaque origin was derived from, read via GetTupleOrPrecursorTupleIfOpaque(). It lets the browser name the site that actually created an opaque context.
Opener chain
The WebContents::GetOpener() links from a popup back to the frame that window.opened it.

Root Cause Analysis

In media_session_impl.cc, for non-data: URLs the media session’s source_title was computed as url_formatter::FormatUrl(url::Origin::Create(url).GetURL(), ...). The hunk does not show the enclosing function’s name. When the playing document had an opaque origin, this formatted an origin with no host, so the Picture-in-Picture window showed no recognizable site. A sandboxed page or an about:blank popup it opened could therefore put up a PiP window that did not truthfully attribute its content to the responsible site. The invariant violated is that security UI attached to a PiP window must always name a meaningful originating site.

The fix keeps the old behavior for non-opaque origins. For opaque ones it walks from the current web_contents() through GetOpener(), using a base::flat_set to stop on cycles. At each step it reads the last committed URL’s tuple or precursor and uses the first valid one as format_url. In the browser tests the window then displays example.com, or the closest ancestor another-site.com.

Key insight
The PiP origin label was derived directly from a possibly-opaque origin, which yields no usable site name. The fix resolves opaque origins to a real tuple via precursor or opener-chain lookup before formatting.

Attack Path

  1. Create an opaque-origin context A page is served with Content-Security-Policy: sandbox allow-scripts allow-popups, and it can also window.open('about:blank') popups. Both setups appear in the regression tests.
  2. Start PiP from the opaque context Script in the popup creates a <video>, plays it, and calls requestPictureInPicture().
  3. Origin label degrades Before the fix, FormatUrl(url::Origin::Create(url).GetURL(), ...) was given an origin with no tuple, so the overlay did not show the true originating site.
  4. User misattribution A floating, always-on-top PiP window with a missing or uninformative origin could be used to show content the user wrongly trusts or attributes to another site (inferred). The patch shows only the label degrading, not a specific spoofed label.

Impact Assessment

A web page can open a Picture-in-Picture window whose browser-provided origin indicator does not identify the site responsible. This is a UI-spoofing issue in the browser’s security UI with no memory-safety component. The only precondition is that the user visits a page that uses a sandboxed or opaque-origin context with media playback. The advisory rates it Low (“Incorrect security UI in PictureInPicture”) and does not say it was exploited in the wild.

Changed Functions

FunctionChangeNotes
ASSERT_TRUE
chrome/browser/picture_in_picture/video_picture_in_picture_window_controller_browsertest.cc
modified

Files Changed

  • chrome/browser/picture_in_picture/video_picture_in_picture_window_controller_browsertest.cc
  • content/browser/media/session/media_session_impl.cc

Audit Directions

  • Origin display from opaque origins
    Search for other security UI (permission prompts, download bubbles, notification attribution, Document PiP, external-protocol dialogs) that formats url::Origin::Create(url).GetURL() or GetLastCommittedOrigin() directly. Check what is shown when the origin is opaque.
  • Opener-derived attribution
    The fallback trusts the opener’s last committed URL. Check whether an opener that has since navigated away can make the PiP window be credited to the wrong site, and whether a frame-level origin (rather than WebContents main-frame URL) should be used for media playing in iframes.
  • Media-session metadata surfaces
    Audit other consumers of MediaSessionImpl metadata (global media controls, lock-screen/OS media integration) for the same opaque-origin labelling gap.
diff --git a/content/browser/media/session/media_session_impl.cc b/content/browser/media/session/media_session_impl.cc
index e9a1474..542bd7e 100644
--- a/content/browser/media/session/media_session_impl.cc
+++ b/content/browser/media/session/media_session_impl.cc
@@ -8,6 +8,7 @@
 #include <memory>
 #include <utility>
 
+#include "base/containers/flat_set.h"
 #include "base/functional/bind.h"
 #include "base/strings/string_util.h"
 #include "base/strings/utf_string_conversions.h"
@@ -1986,8 +1987,38 @@
       source_title =
           content_client->GetLocalizedString(IDS_MEDIA_SESSION_DATA_SOURCE);
     } else {
+      url::Origin origin = url::Origin::Create(url);
+      GURL format_url = origin.GetURL();
+
+      // If the origin is opaque, use its precursor origin if available.
+      // Otherwise, traverse the opener chain to find the closest ancestor with
+      // a valid precursor origin. This ensures we display a recognizable origin
+      // to the user.
+      if (origin.opaque()) {
+        WebContents* current_web_contents = web_contents();
+        base::flat_set<WebContents*> seen_web_contents;
+
+        while (current_web_contents &&
+               !seen_web_contents.contains(current_web_contents)) {
+          seen_web_contents.insert(current_web_contents);
+
+          url::Origin current_origin =
+              url::Origin::Create(current_web_contents->GetLastCommittedURL());
+          const auto& precursor =
+              current_origin.GetTupleOrPrecursorTupleIfOpaque();
+          if (precursor.IsValid()) {
+            format_url = precursor.GetURL();
+            break;
+          }
+
+          RenderFrameHost* opener = current_web_contents->GetOpener();
+          current_web_contents =
+              opener ? WebContents::FromRenderFrameHost(opener) : nullptr;
+        }
+      }
+
       source_title = url_formatter::FormatUrl(
-          url::Origin::Create(url).GetURL(),
+          format_url,
           url_formatter::kFormatUrlOmitDefaults |
               url_formatter::kFormatUrlOmitHTTPS |
               url_formatter::kFormatUrlOmitTrivialSubdomains,
diff --git a/chrome/browser/picture_in_picture/video_picture_in_picture_window_controller_browsertest.cc b/chrome/browser/picture_in_picture/video_picture_in_picture_window_controller_browsertest.cc
index 617f570..3135ccc 100644
--- a/chrome/browser/picture_in_picture/video_picture_in_picture_window_controller_browsertest.cc
+++ b/chrome/browser/picture_in_picture/video_picture_in_picture_window_controller_browsertest.cc
@@ -9,6 +9,7 @@
 #include "base/memory/raw_ptr.h"
 #include "base/path_service.h"
 #include "base/scoped_observation.h"
+#include "base/strings/string_util.h"
 #include "base/test/bind.h"
 #include "base/test/run_until.h"
 #include "base/test/scoped_feature_list.h"
@@ -2424,6 +2425,193 @@
   EXPECT_TRUE(GetOverlayWindow()->AreTitleAndScrimVisibleForTesting());
 }
 
+IN_PROC_BROWSER_TEST_F(VideoPictureInPictureWindowControllerBrowserTest,
+                       SourceTitle_OpaqueFallback) {
+  const std::string kTestHost = "example.com";
+  const std::u16string kExpectedTitlePrefix = base::ASCIIToUTF16(kTestHost);
+
+  // Open a sandboxed page, which will have an opaque origin.
+  GURL sandboxed_main_url =
+      embedded_test_server()->GetURL(kTestHost,
+                                     "/set-header?Content-Security-Policy: "
+                                     "sandbox allow-scripts allow-popups");
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), sandboxed_main_url));
+
+  content::WebContents* active_web_contents =
+      browser()->tab_strip_model()->GetActiveWebContents();
+
+  // Verify the main frame has an opaque origin.
+  ASSERT_TRUE(active_web_contents->GetPrimaryMainFrame()
+                  ->GetLastCommittedOrigin()
+                  .opaque());
+
+  // Open an about:blank popup from the sandboxed main frame.
+  content::WebContents* popup_contents;
+  {
+    content::WebContentsAddedObserver new_contents_observer;
+    ASSERT_TRUE(ExecJs(active_web_contents, "window.open('about:blank');"));
+    popup_contents = new_contents_observer.GetWebContents();
+  }
+
+  // Verify that the popup also has an opaque origin
+  // and has established the opener relationship.
+  ASSERT_TRUE(
+      popup_contents->GetPrimaryMainFrame()->GetLastCommittedOrigin().opaque());
+  ASSERT_EQ(active_web_contents->GetPrimaryMainFrame(),
+            popup_contents->GetOpener());
+
+  // Inject a video element and play it in the popup.
+  GURL video_url =
+      embedded_test_server()->GetURL(kTestHost, "/media/bear.webm");
+  std::string script = base::ReplaceStringPlaceholders(
+      R"(
+        const video = document.createElement('video');
+        video.src = '$1';
+        video.loop = true;
+        document.body.appendChild(video);
+        video.play().then(() => video.requestPictureInPicture());
+      )",
+      {video_url.spec()}, nullptr);
+  ASSERT_TRUE(ExecJs(popup_contents, script));
+
+  // Wait until the Picture-in-Picture window is visible and its source title
+  // correctly falls back to the opener's origin (example.com).
+  SetUpWindowController(popup_contents);
+  ASSERT_TRUE(base::test::RunUntil([&]() {
+    auto* overlay_window = GetOverlayWindow();
+    return overlay_window && overlay_window->IsVisible() &&
+           overlay_window->origin_for_testing() &&
+           base::StartsWith(overlay_window->origin_for_testing()->GetText(),
+                            kExpectedTitlePrefix);
+  }));
+}
+
+IN_PROC_BROWSER_TEST_F(VideoPictureInPictureWindowControllerBrowserTest,
+                       SourceTitle_NestedOpaqueFallback) {
+  const std::string kTestHost = "example.com";
+  const std::u16string kExpectedTitlePrefix = base::ASCIIToUTF16(kTestHost);
+
+  // Open a sandboxed page, which will have an opaque origin.
+  GURL sandboxed_main_url =
+      embedded_test_server()->GetURL(kTestHost,
+                                     "/set-header?Content-Security-Policy: "
+                                     "sandbox allow-scripts allow-popups");
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), sandboxed_main_url));
+
+  content::WebContents* active_web_contents =
+      browser()->tab_strip_model()->GetActiveWebContents();
+
+  // Open an about:blank popup from the sandboxed main frame.
+  content::WebContents* popup1_contents;
+  {
+    content::WebContentsAddedObserver observer;
+    ASSERT_TRUE(ExecJs(active_web_contents, "window.open('about:blank');"));
+    popup1_contents = observer.GetWebContents();
+  }
+
+  // Open another about:blank popup from the first popup.
+  content::WebContents* popup2_contents;
+  {
+    content::WebContentsAddedObserver observer;
+    ASSERT_TRUE(ExecJs(popup1_contents, "window.open('about:blank');"));
+    popup2_contents = observer.GetWebContents();
+  }
+
+  // Inject a video element and play it in the nested popup.
+  GURL video_url =
+      embedded_test_server()->GetURL(kTestHost, "/media/bear.webm");
+  std::string script = base::ReplaceStringPlaceholders(
+      R"(
+        const video = document.createElement('video');
+        video.src = '$1';
+        video.loop = true;
+        document.body.appendChild(video);
+        video.play().then(() => video.requestPictureInPicture());
+      )",
+      {video_url.spec()}, nullptr);
+  ASSERT_TRUE(ExecJs(popup2_contents, script));
+
+  // Wait until the Picture-in-Picture window is visible and its source title
+  // correctly falls back through the nested openers to the original origin
+  // (example.com).
+  SetUpWindowController(popup2_contents);
+  ASSERT_TRUE(base::test::RunUntil([&]() {
+    auto* overlay_window = GetOverlayWindow();
+    return overlay_window && overlay_window->IsVisible() &&
+           overlay_window->origin_for_testing() &&
+           base::StartsWith(overlay_window->origin_for_testing()->GetText(),
+                            kExpectedTitlePrefix);
+  }));
+}
+
+IN_PROC_BROWSER_TEST_F(VideoPictureInPictureWindowControllerBrowserTest,
+                       SourceTitle_ClosestAncestorFallback) {
+  const std::string kHost1 = "example.com";
+  const std::string kHost2 = "another-site.com";
+  const std::u16string kExpectedTitlePrefix = base::ASCIIToUTF16(kHost2);
+
+  // Open Host 1 sandboxed.
+  GURL url1 =
+      embedded_test_server()->GetURL(kHost1,
+                                     "/set-header?Content-Security-Policy: "
+                                     "sandbox allow-scripts allow-popups");
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url1));
+  content::WebContents* active_web_contents =
+      browser()->tab_strip_model()->GetActiveWebContents();
+
+  // Open about:blank (Popup 1).
+  content::WebContents* popup1_contents;
+  {
+    content::WebContentsAddedObserver observer;
+    ASSERT_TRUE(ExecJs(active_web_contents, "window.open('about:blank');"));
+    popup1_contents = observer.GetWebContents();
+  }
+
+  // Navigate Popup 1 to Host 2 sandboxed.
+  GURL url2 =
+      embedded_test_server()->GetURL(kHost2,
+                                     "/set-header?Content-Security-Policy: "
+                                     "sandbox allow-scripts allow-popups");
+  {
+    content::TestNavigationObserver nav_observer(popup1_contents);
+    ASSERT_TRUE(ExecJs(popup1_contents,
+                       base::StringPrintf("window.location.href = '%s';",
+                                          url2.spec().c_str())));
+    nav_observer.Wait();
+  }
+
+  // Open another about:blank (Popup 2) from Popup 1 (Host 2).
+  content::WebContents* popup2_contents;
+  {
+    content::WebContentsAddedObserver observer;
+    ASSERT_TRUE(ExecJs(popup1_contents, "window.open('about:blank');"));
+    popup2_contents = observer.GetWebContents();
+  }
+
+  // Play video in Popup 2.
+  GURL video_url = embedded_test_server()->GetURL(kHost2, "/media/bear.webm");
+  std::string script = base::ReplaceStringPlaceholders(
+      R"(
+        const video = document.createElement('video');
+        video.src = '$1';
+        video.loop = true;
+        document.body.appendChild(video);
+        video.play().then(() => video.requestPictureInPicture());
+      )",
+      {video_url.spec()}, nullptr);
+  ASSERT_TRUE(ExecJs(popup2_contents, script));
+
+  // Verify source title is Host 2 (the closest opener with a valid precursor).
+  SetUpWindowController(popup2_contents);
+  ASSERT_TRUE(base::test::RunUntil([&]() {
+    auto* overlay_window = GetOverlayWindow();
+    return overlay_window && overlay_window->IsVisible() &&
+           overlay_window->origin_for_testing() &&
+           base::StartsWith(overlay_window->origin_for_testing()->GetText(),
+                            kExpectedTitlePrefix);
+  }));
+}
+
 struct InteractionTestParam {
   ui::EventType event_type;
   bool title_should_be_visible;
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/picture_in_picture/video_picture_in_picture_window_controller_browsertest.cc b/chrome/browser/picture_in_picture/video_picture_in_picture_window_controller_browsertest.cc
index 617f570..3135ccc 100644
--- a/chrome/browser/picture_in_picture/video_picture_in_picture_window_controller_browsertest.cc
+++ b/chrome/browser/picture_in_picture/video_picture_in_picture_window_controller_browsertest.cc
@@ -9,6 +9,7 @@
 #include "base/memory/raw_ptr.h"
 #include "base/path_service.h"
 #include "base/scoped_observation.h"
+#include "base/strings/string_util.h"
 #include "base/test/bind.h"
 #include "base/test/run_until.h"
 #include "base/test/scoped_feature_list.h"
@@ -2424,6 +2425,193 @@
   EXPECT_TRUE(GetOverlayWindow()->AreTitleAndScrimVisibleForTesting());
 }
 
+IN_PROC_BROWSER_TEST_F(VideoPictureInPictureWindowControllerBrowserTest,
+                       SourceTitle_OpaqueFallback) {
+  const std::string kTestHost = "example.com";
+  const std::u16string kExpectedTitlePrefix = base::ASCIIToUTF16(kTestHost);
+
+  // Open a sandboxed page, which will have an opaque origin.
+  GURL sandboxed_main_url =
+      embedded_test_server()->GetURL(kTestHost,
+                                     "/set-header?Content-Security-Policy: "
+                                     "sandbox allow-scripts allow-popups");
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), sandboxed_main_url));
+
+  content::WebContents* active_web_contents =
+      browser()->tab_strip_model()->GetActiveWebContents();
+
+  // Verify the main frame has an opaque origin.
+  ASSERT_TRUE(active_web_contents->GetPrimaryMainFrame()
+                  ->GetLastCommittedOrigin()
+                  .opaque());
+
+  // Open an about:blank popup from the sandboxed main frame.
+  content::WebContents* popup_contents;
+  {
+    content::WebContentsAddedObserver new_contents_observer;
+    ASSERT_TRUE(ExecJs(active_web_contents, "window.open('about:blank');"));
+    popup_contents = new_contents_observer.GetWebContents();
+  }
+
+  // Verify that the popup also has an opaque origin
+  // and has established the opener relationship.
+  ASSERT_TRUE(
+      popup_contents->GetPrimaryMainFrame()->GetLastCommittedOrigin().opaque());
+  ASSERT_EQ(active_web_contents->GetPrimaryMainFrame(),
+            popup_contents->GetOpener());
+
+  // Inject a video element and play it in the popup.
+  GURL video_url =
+      embedded_test_server()->GetURL(kTestHost, "/media/bear.webm");
+  std::string script = base::ReplaceStringPlaceholders(
+      R"(
+        const video = document.createElement('video');
+        video.src = '$1';
+        video.loop = true;
+        document.body.appendChild(video);
+        video.play().then(() => video.requestPictureInPicture());
+      )",
+      {video_url.spec()}, nullptr);
+  ASSERT_TRUE(ExecJs(popup_contents, script));
+
+  // Wait until the Picture-in-Picture window is visible and its source title
+  // correctly falls back to the opener's origin (example.com).
+  SetUpWindowController(popup_contents);
+  ASSERT_TRUE(base::test::RunUntil([&]() {
+    auto* overlay_window = GetOverlayWindow();
+    return overlay_window && overlay_window->IsVisible() &&
+           overlay_window->origin_for_testing() &&
+           base::StartsWith(overlay_window->origin_for_testing()->GetText(),
+                            kExpectedTitlePrefix);
+  }));
+}
+
+IN_PROC_BROWSER_TEST_F(VideoPictureInPictureWindowControllerBrowserTest,
+                       SourceTitle_NestedOpaqueFallback) {
+  const std::string kTestHost = "example.com";
+  const std::u16string kExpectedTitlePrefix = base::ASCIIToUTF16(kTestHost);
+
+  // Open a sandboxed page, which will have an opaque origin.
+  GURL sandboxed_main_url =
+      embedded_test_server()->GetURL(kTestHost,
+                                     "/set-header?Content-Security-Policy: "
+                                     "sandbox allow-scripts allow-popups");
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), sandboxed_main_url));
+
+  content::WebContents* active_web_contents =
+      browser()->tab_strip_model()->GetActiveWebContents();
+
+  // Open an about:blank popup from the sandboxed main frame.
+  content::WebContents* popup1_contents;
+  {
+    content::WebContentsAddedObserver observer;
+    ASSERT_TRUE(ExecJs(active_web_contents, "window.open('about:blank');"));
+    popup1_contents = observer.GetWebContents();
+  }
+
+  // Open another about:blank popup from the first popup.
+  content::WebContents* popup2_contents;
+  {
+    content::WebContentsAddedObserver observer;
+    ASSERT_TRUE(ExecJs(popup1_contents, "window.open('about:blank');"));
+    popup2_contents = observer.GetWebContents();
+  }
+
+  // Inject a video element and play it in the nested popup.
+  GURL video_url =
+      embedded_test_server()->GetURL(kTestHost, "/media/bear.webm");
+  std::string script = base::ReplaceStringPlaceholders(
+      R"(
+        const video = document.createElement('video');
+        video.src = '$1';
+        video.loop = true;
+        document.body.appendChild(video);
+        video.play().then(() => video.requestPictureInPicture());
+      )",
+      {video_url.spec()}, nullptr);
+  ASSERT_TRUE(ExecJs(popup2_contents, script));
+
+  // Wait until the Picture-in-Picture window is visible and its source title
+  // correctly falls back through the nested openers to the original origin
+  // (example.com).
+  SetUpWindowController(popup2_contents);
+  ASSERT_TRUE(base::test::RunUntil([&]() {
+    auto* overlay_window = GetOverlayWindow();
+    return overlay_window && overlay_window->IsVisible() &&
+           overlay_window->origin_for_testing() &&
+           base::StartsWith(overlay_window->origin_for_testing()->GetText(),
+                            kExpectedTitlePrefix);
+  }));
+}
+
+IN_PROC_BROWSER_TEST_F(VideoPictureInPictureWindowControllerBrowserTest,
+                       SourceTitle_ClosestAncestorFallback) {
+  const std::string kHost1 = "example.com";
+  const std::string kHost2 = "another-site.com";
+  const std::u16string kExpectedTitlePrefix = base::ASCIIToUTF16(kHost2);
+
+  // Open Host 1 sandboxed.
+  GURL url1 =
+      embedded_test_server()->GetURL(kHost1,
+                                     "/set-header?Content-Security-Policy: "
+                                     "sandbox allow-scripts allow-popups");
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url1));
+  content::WebContents* active_web_contents =
+      browser()->tab_strip_model()->GetActiveWebContents();
+
+  // Open about:blank (Popup 1).
+  content::WebContents* popup1_contents;
+  {
+    content::WebContentsAddedObserver observer;
+    ASSERT_TRUE(ExecJs(active_web_contents, "window.open('about:blank');"));
+    popup1_contents = observer.GetWebContents();
+  }
+
+  // Navigate Popup 1 to Host 2 sandboxed.
+  GURL url2 =
+      embedded_test_server()->GetURL(kHost2,
+                                     "/set-header?Content-Security-Policy: "
+                                     "sandbox allow-scripts allow-popups");
+  {
+    content::TestNavigationObserver nav_observer(popup1_contents);
+    ASSERT_TRUE(ExecJs(popup1_contents,
+                       base::StringPrintf("window.location.href = '%s';",
+                                          url2.spec().c_str())));
+    nav_observer.Wait();
+  }
+
+  // Open another about:blank (Popup 2) from Popup 1 (Host 2).
+  content::WebContents* popup2_contents;
+  {
+    content::WebContentsAddedObserver observer;
+    ASSERT_TRUE(ExecJs(popup1_contents, "window.open('about:blank');"));
+    popup2_contents = observer.GetWebContents();
+  }
+
+  // Play video in Popup 2.
+  GURL video_url = embedded_test_server()->GetURL(kHost2, "/media/bear.webm");
+  std::string script = base::ReplaceStringPlaceholders(
+      R"(
+        const video = document.createElement('video');
+        video.src = '$1';
+        video.loop = true;
+        document.body.appendChild(video);
+        video.play().then(() => video.requestPictureInPicture());
+      )",
+      {video_url.spec()}, nullptr);
+  ASSERT_TRUE(ExecJs(popup2_contents, script));
+
+  // Verify source title is Host 2 (the closest opener with a valid precursor).
+  SetUpWindowController(popup2_contents);
+  ASSERT_TRUE(base::test::RunUntil([&]() {
+    auto* overlay_window = GetOverlayWindow();
+    return overlay_window && overlay_window->IsVisible() &&
+           overlay_window->origin_for_testing() &&
+           base::StartsWith(overlay_window->origin_for_testing()->GetText(),
+                            kExpectedTitlePrefix);
+  }));
+}
+
 struct InteractionTestParam {
   ui::EventType event_type;
   bool title_should_be_visible;
Loading diff…

Original Bug Report

reported by [email protected]

PIP Origin Attribution Missing When Triggered from `about:blank` via Injected JavaScript


Report description

PIP Origin Attribution Missing When Triggered from about:blank via Injected JavaScript


Bug location

Where do you want to report your vulnerability?

Chrome VRP – Report security issues affecting the Chrome browser. See program rules


The problem

Please describe the technical details of the vulnerability

Summary When a popup window is opened at about:blank and dynamically populated using JavaScript from its opener, initiating a Picture-in-Picture (PiP) request from this context results in missing or incorrect origin attribution in the PiP window UI. Instead of displaying the actual execution origin, the PiP window appears without any visible site attribution.

Steps to Reproduce

  1. Open the PoC URL: https://bughunter-6.github.io/SummaTest/emptytest.html
  2. Click the button to open the popup window.
  3. The popup opens at about:blank and is populated via injected JavaScript from the opener.
  4. Click the Picture-in-Picture button inside the popup.
  5. Observe the PiP window UI.
  6. The PiP window does not display any origin or site attribution.

POC URL: https://drive.google.com/file/d/1AJyG06LioG1PsaPaX5ZSVJXceCHe61P8/view?usp=sharing

Observed Behavior The Picture-in-Picture window launches without showing the true origin responsible for the PiP request when the request is triggered from an about:blank document, despite the JavaScript executing under the opener’s origin.

Expected Behavior The PiP window should consistently and clearly display the actual execution origin that initiated Picture-in-Picture, even if the visible document URL is about:blank. Blank or origin-less PiP UI should be avoided to ensure proper user awareness.

Impact analysis

This behavior allows attacker-controlled sites to present PiP content without exposing the real originating domain, enabling origin confusion and deceptive overlays. Users may trust a PiP window believing it is system-level or neutral, while it is actually controlled by a malicious site. An attacker could abuse this to display persistent phishing prompts, fake system alerts, or misleading media overlays while masking the true source. When combined with fullscreen or pointer lock, this significantly increases the risk of UI spoofing and social engineering attacks.


The cause

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

Version 143.0.7499.193 (Official Build) (arm64)

No, it is not related to a crash.

Choose the type of vulnerability

Security UI Spoofing

How would you like to be publicly acknowledged for your report?

Barath Stalin K( https://in.linkedin.com/in/barathstalin)

View on issue tracker