Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect security UI in PictureInPicture
DescriptionIncorrect security UI in PictureInPicture
ComponentPictureInPicture
Bug ClassLogic Error
Tracker475238879
Fix commitb7b72d7dfb58 (chromium/src) +220/-1
CISA KEVNot listed
CreditedBarath Stalin K
Disclosed2026-03-10

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
From b7b72d7dfb58395d16190c437496f0f2d3d97c2d Mon Sep 17 00:00:00 2001
From: Benjamin Keen <bkeen@google.com>
Date: Fri, 23 Jan 2026 14:18:02 -0800
Subject: [PATCH] Handle opaque origins when determining the metadata source title

Currently, media from opaque origins can have an empty source title.
This change implements a recursive fallback strategy that traverses the
opener chain to find the closest ancestor with a non-empty precursor.

This ensures a recognizable domain is displayed to the user, whenever
possible.

Bug: 475238879
Change-Id: Ic729e6fd501a430a89039463d19b37f2c5efbe68
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7509243
Reviewed-by: Tommy Steimel <steimel@chromium.org>
Commit-Queue: Benjamin Keen <bkeen@google.com>
Cr-Commit-Position: refs/heads/main@{#1573950}
---

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.
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 se...@gmail.com

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