CVE-2026-3942
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ASSERT_TRUEchrome/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
Patch
From b7b72d7dfb58395d16190c437496f0f2d3d97c2d Mon Sep 17 00:00:00 2001 From: Benjamin Keen <[email protected]> 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 <[email protected]> Commit-Queue: Benjamin Keen <[email protected]> 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.
Regression Test / PoC
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;
Original Bug Report
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
- Open the PoC URL: https://bughunter-6.github.io/SummaTest/emptytest.html
- Click the button to open the popup window.
- The popup opens at
about:blankand is populated via injected JavaScript from the opener. - Click the Picture-in-Picture button inside the popup.
- Observe the PiP window UI.
- 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)
Is the security issue related to a crash?
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)