Medium watch CVSS 4.3 chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Picture In Picture
DescriptionInappropriate implementation in Picture In Picture
ComponentPicture In Picture
Sub-componentPip
Bug ClassLogic Error
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.
Tracker384050903
Fix commit923d0eca9ed8 (chromium/src) +782/-1
CWECWE-79 (Cross-site scripting)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:N
CISA KEVNot listed
CreditedUmar Farooq
Reported2024-12-14 · bounty $1000 (per vendor advisory)
Fix landed2025-06-10 (public git)
Disclosed2025-08-05
Patch gap56 days — fix public in source before the advisory

Background

Picture-in-picture (PiP) window
an always-on-top window (video VideoOverlayWindowViews or document PictureInPictureBrowserFrameView) that floats over other browser and OS UI.
`PictureInPictureWindowManager`
the singleton that tracks the active PiP window and mediates policies such as disallowing or tucking PiP when other UI needs precedence.
File dialog occlusion
the security-relevant condition where a floating PiP window covers a native file-selection dialog, potentially misleading the user about what they are interacting with.
`ScopedTuckPictureInPicture` / `SetForcedTucking`
an RAII scope that, while alive, forces the tracked PiP window off to the side of the screen via PictureInPictureWindow::SetForcedTucking(true), restoring position on destruction.

Root Cause Analysis

The vulnerable behavior is that a picture-in-picture window could occlude a native file dialog, an inappropriate-implementation/UI-security issue in the Picture In Picture component. As the commit message notes, an earlier mitigation closed all PiP windows whenever a file dialog opened, but that caused its own problems and was disabled — leaving file dialogs once again occludable.

The fix introduces a “tucking” mechanism: FileSelectHelper now, when ShouldFileDialogTuckPictureInPicture(web_contents_) returns true, holds a ScopedTuckPictureInPicture for the dialog’s lifetime, which drives PictureInPictureWindowManager to call SetForcedTucking(true) and move the PiP window offscreen instead of closing it. ShouldFileDialogTuckPictureInPicture deliberately returns false when the file dialog is itself owned by a document PiP window’s child WebContents, avoiding tucking the very window hosting the dialog. Reference-counting via number_of_existing_scoped_tuck_picture_in_pictures_ and IsPictureInPictureForceTucked() ensures the window is only untucked once all tuck scopes are gone, so a still-open dialog is never re-occluded.

Key insight
A PiP window could cover a native file dialog because the prior close-on-dialog mitigation had been disabled; the fix restores protection non-destructively by tucking the PiP window offscreen for the dialog’s lifetime rather than closing it.

Attack Path

  1. Open a floating PiP window A page opens a video or document picture-in-picture window that renders always-on-top.
  2. Trigger a file dialog The page induces a native file-selection dialog (e.g., via a file input), which the user must read to make a trust decision.
  3. Occlude the dialog Pre-patch, the PiP window remains positioned over the file dialog, obscuring or spoofing the dialog’s content or controls so the user misjudges what they are approving.
  4. User interaction on the obscured dialog The user acts on the file dialog while it is visually occluded by attacker-controlled PiP content; concrete exploitation into unintended file disclosure is (inferred), as the patch demonstrates only the occlusion condition and its remediation.

Impact Assessment

The attacker gains the ability to visually occlude a native file dialog with an always-on-top PiP window in the browser UI, an inappropriate-implementation issue that can undermine the user’s ability to correctly perceive a security-relevant dialog. Preconditions are an open PiP window and a file dialog not hosted inside a document PiP window, with the media::kFileDialogsTuckPictureInPicture feature governing the mitigation. The advisory reports this as inappropriate implementation in Picture In Picture; no exploited-in-the-wild status is stated, and any escalation to actual file disclosure or spoofing-driven compromise is (inferred).

Changed Functions

FunctionChangeNotes
Profile
chrome/browser/file_select_helper.h
modified
ScopedDisallowPictureInPicture
chrome/browser/file_select_helper.h
modified
ScopedTuckPictureInPicture
chrome/browser/file_select_helper.h
modified
FileSelectListener
chrome/browser/file_select_helper.h
modified
PictureInPictureWindow
chrome/browser/picture_in_picture/picture_in_picture_window.h
modified
if
chrome/browser/picture_in_picture/picture_in_picture_window_manager.cc
modified

Files Changed

  • chrome/browser/file_select_helper.cc
  • chrome/browser/file_select_helper.h
  • chrome/browser/picture_in_picture/BUILD.gn
  • chrome/browser/picture_in_picture/picture_in_picture_window.h
  • chrome/browser/picture_in_picture/picture_in_picture_window_manager.cc

Audit Directions

  • Always-on-top UI over security dialogs
    Review every floating/always-on-top surface (PiP, GLIC, notifications) to ensure it cannot occlude native security-critical dialogs such as file, permission, and authentication prompts.
  • Disabled or reverted mitigations
    Search for features that were disabled after causing regressions; a reverted protection may leave the original vulnerability open until a replacement lands.
  • Scoped/refcounted UI state
    Audit RAII scopes that toggle shared window state (tucking, disallowing) to verify correct reference counting so state is not cleared while another consumer still requires it.
From 923d0eca9ed86960af9e735f8a4b2271fa17fb37 Mon Sep 17 00:00:00 2001
From: Tommy Steimel <[email protected]>
Date: Tue, 10 Jun 2025 11:52:42 -0700
Subject: [PATCH] [pip] Tuck picture-in-picture windows when a file dialog is open

Picture-in-picture windows can occlude file dialogs, and initially we
tried to prevent this by closing all pip windows whenever a file
dialog was open. This created its own set of problems, so that feature
was disabled.

This is a replacement of that feature: instead of closing pip windows,
this moves them off to the side of the screen, preventing them from
obscuring the file dialog without having to actually close them.

go/picture-in-picture-tucking-design-doc

Bug: 403792431, 384050903, 415979072
Change-Id: I280333c32a02aee3c345c839e88848de6820ebe2
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6449682
Reviewed-by: Evan Liu <[email protected]>
Commit-Queue: Tommy Steimel <[email protected]>
Reviewed-by: Frank Liberato <[email protected]>
Reviewed-by: Fr <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1471975}
---

diff --git a/chrome/browser/file_select_helper.cc b/chrome/browser/file_select_helper.cc
index f03a163..442f428 100644
--- a/chrome/browser/file_select_helper.cc
+++ b/chrome/browser/file_select_helper.cc
@@ -60,6 +60,7 @@
 #else
 #include "chrome/browser/picture_in_picture/picture_in_picture_window_manager.h"
 #include "chrome/browser/picture_in_picture/scoped_disallow_picture_in_picture.h"
+#include "chrome/browser/picture_in_picture/scoped_tuck_picture_in_picture.h"
 #endif  // BUILDFLAG(IS_ANDROID)
 
 using blink::mojom::FileChooserFileInfo;
@@ -614,6 +615,10 @@
           ->ShouldFileDialogBlockPictureInPicture(web_contents_)) {
     scoped_disallow_picture_in_picture_ =
         std::make_unique<ScopedDisallowPictureInPicture>();
+  } else if (PictureInPictureWindowManager::GetInstance()
+                 ->ShouldFileDialogTuckPictureInPicture(web_contents_)) {
+    scoped_tuck_picture_in_picture_ =
+        std::make_unique<ScopedTuckPictureInPicture>();
   }
 #endif  // !BUILDFLAG(IS_ANDROID)
 
@@ -791,6 +796,7 @@
 
 #if !BUILDFLAG(IS_ANDROID)
   scoped_disallow_picture_in_picture_.reset();
+  scoped_tuck_picture_in_picture_.reset();
 #endif  // !BUILDFLAG(IS_ANDROID)
 
   Release();
diff --git a/chrome/browser/file_select_helper.h b/chrome/browser/file_select_helper.h
index e5b8c7d6..2025d39 100644
--- a/chrome/browser/file_select_helper.h
+++ b/chrome/browser/file_select_helper.h
@@ -28,6 +28,7 @@
 
 class Profile;
 class ScopedDisallowPictureInPicture;
+class ScopedTuckPictureInPicture;
 
 namespace content {
 class FileSelectListener;
@@ -334,6 +335,9 @@
   // When not null, this prevents picture-in-picture windows from opening.
   std::unique_ptr<ScopedDisallowPictureInPicture>
       scoped_disallow_picture_in_picture_;
+
+  // When not null, this tucks picture-in-picture windows out of the way.
+  std::unique_ptr<ScopedTuckPictureInPicture> scoped_tuck_picture_in_picture_;
 #endif  // !BUILDFLAG(IS_ANDROID)
 
 #if BUILDFLAG(IS_CHROMEOS)
diff --git a/chrome/browser/picture_in_picture/BUILD.gn b/chrome/browser/picture_in_picture/BUILD.gn
index 3d018a9..3e2eb8a 100644
--- a/chrome/browser/picture_in_picture/BUILD.gn
+++ b/chrome/browser/picture_in_picture/BUILD.gn
@@ -36,9 +36,11 @@
       "picture_in_picture_occlusion_observer.h",
       "picture_in_picture_occlusion_tracker.h",
       "picture_in_picture_occlusion_tracker_observer.h",
+      "picture_in_picture_window.h",
       "picture_in_picture_window_manager_uma_helper.h",
       "scoped_disallow_picture_in_picture.h",
       "scoped_picture_in_picture_occlusion_observation.h",
+      "scoped_tuck_picture_in_picture.h",
     ]
     public_deps += [
       "//chrome/browser/ui/tabs:tab_strip_model_observer",
@@ -102,6 +104,7 @@
       "picture_in_picture_window_manager_uma_helper.cc",
       "scoped_disallow_picture_in_picture.cc",
       "scoped_picture_in_picture_occlusion_observation.cc",
+      "scoped_tuck_picture_in_picture.cc",
     ]
     deps += [
       "//chrome/app:generated_resources",
diff --git a/chrome/browser/picture_in_picture/picture_in_picture_window.h b/chrome/browser/picture_in_picture/picture_in_picture_window.h
new file mode 100644
index 0000000..f017e54
--- /dev/null
+++ b/chrome/browser/picture_in_picture/picture_in_picture_window.h
@@ -0,0 +1,26 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_PICTURE_IN_PICTURE_PICTURE_IN_PICTURE_WINDOW_H_
+#define CHROME_BROWSER_PICTURE_IN_PICTURE_PICTURE_IN_PICTURE_WINDOW_H_
+
+// A PictureInPictureWindow is an always-on-top window that displays content to
+// the user. There are two types of PictureInPictureWindows: video
+// picture-in-picture windows (`VideoOverlayWindowViews`) and document
+// picture-in-picture (`PictureInPictureBrowserFrameView`). This class has
+// shared logic for both to be controlled directly by the
+// PictureInPictureWindowManager.
+class PictureInPictureWindow {
+ public:
+  // When `tuck` is true, this forces the PictureInPictureWindow to be tucked
+  // offscreen. When `tuck` is false, it returns the PictureInPictureWindow to
+  // its original position.
+  virtual void SetForcedTucking(bool tuck) = 0;
+
+ protected:
+  PictureInPictureWindow() = default;
+  virtual ~PictureInPictureWindow() = default;
+};
+
+#endif  // CHROME_BROWSER_PICTURE_IN_PICTURE_PICTURE_IN_PICTURE_WINDOW_H_
diff --git a/chrome/browser/picture_in_picture/picture_in_picture_window_manager.cc b/chrome/browser/picture_in_picture/picture_in_picture_window_manager.cc
index 395f622..90346363 100644
--- a/chrome/browser/picture_in_picture/picture_in_picture_window_manager.cc
+++ b/chrome/browser/picture_in_picture/picture_in_picture_window_manager.cc
@@ -28,6 +28,7 @@
 // TODO(crbug.com/421608904): include auto_picture_in_picture_tab_helper for
 // Android.
 #include "chrome/browser/picture_in_picture/auto_picture_in_picture_tab_helper.h"
+#include "chrome/browser/picture_in_picture/picture_in_picture_window.h"
 #include "media/base/media_switches.h"
 #include "net/base/url_util.h"
 #include "third_party/blink/public/common/features.h"
@@ -733,6 +734,61 @@
   number_of_existing_scoped_disallow_picture_in_pictures_--;
 }
 
+void PictureInPictureWindowManager::OnPictureInPictureWindowShown(
+    PictureInPictureWindow* window) {
+  picture_in_picture_window_ = window;
+  if (IsPictureInPictureForceTucked()) {
+    picture_in_picture_window_->SetForcedTucking(true);
+    RecordPictureInPictureTucked(PictureInPictureTuckedType::kNewWindowTucked);
+  }
+}
+
+void PictureInPictureWindowManager::OnPictureInPictureWindowHidden(
+    PictureInPictureWindow* window) {
+  if (picture_in_picture_window_ == window) {
+    picture_in_picture_window_ = nullptr;
+  }
+}
+
+bool PictureInPictureWindowManager::ShouldFileDialogTuckPictureInPicture(
+    content::WebContents* owner_web_contents) {
+  if (!base::FeatureList::IsEnabled(media::kFileDialogsTuckPictureInPicture)) {
+    return false;
+  }
+
+  // File dialogs opened inside document picture-in-picture windows should not
+  // tuck picture-in-picture.
+  if (pip_window_controller_ &&
+      pip_window_controller_->GetChildWebContents() == owner_web_contents) {
+    return false;
+  }
+
+  return true;
+}
+
+void PictureInPictureWindowManager::OnScopedTuckPictureInPictureCreated(
+    base::PassKey<ScopedTuckPictureInPicture>) {
+  number_of_existing_scoped_tuck_picture_in_pictures_++;
+  if (picture_in_picture_window_) {
+    picture_in_picture_window_->SetForcedTucking(true);
+    RecordPictureInPictureTucked(
+        PictureInPictureTuckedType::kExistingWindowTucked);
+  }
+}
+
+void PictureInPictureWindowManager::OnScopedTuckPictureInPictureDestroyed(
+    base::PassKey<ScopedTuckPictureInPicture>) {
+  CHECK_NE(number_of_existing_scoped_tuck_picture_in_pictures_, 0u);
+  number_of_existing_scoped_tuck_picture_in_pictures_--;
+  if (picture_in_picture_window_ && !IsPictureInPictureForceTucked()) {
+    picture_in_picture_window_->SetForcedTucking(false);
+  }
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/picture_in_picture/picture_in_picture_window_manager_unittest.cc b/chrome/browser/picture_in_picture/picture_in_picture_window_manager_unittest.cc
index ef8e05f..aaa616e8 100644
--- a/chrome/browser/picture_in_picture/picture_in_picture_window_manager_unittest.cc
+++ b/chrome/browser/picture_in_picture/picture_in_picture_window_manager_unittest.cc
@@ -20,8 +20,10 @@
 #include "base/test/metrics/histogram_tester.h"
 #include "base/test/simple_test_tick_clock.h"
 #include "chrome/browser/picture_in_picture/auto_picture_in_picture_tab_helper.h"
+#include "chrome/browser/picture_in_picture/picture_in_picture_window.h"
 #include "chrome/browser/picture_in_picture/picture_in_picture_window_manager_uma_helper.h"
 #include "chrome/browser/picture_in_picture/scoped_disallow_picture_in_picture.h"
+#include "chrome/browser/picture_in_picture/scoped_tuck_picture_in_picture.h"
 #include "media/base/media_switches.h"
 #include "ui/views/bubble/bubble_dialog_delegate_view.h"
 #include "ui/views/view.h"
@@ -65,6 +67,25 @@
   MOCK_METHOD(std::optional<url::Origin>, GetOrigin, (), (override));
 };
 
+#if !BUILDFLAG(IS_ANDROID)
+class MockPictureInPictureWindow : public PictureInPictureWindow {
+ public:
+  MockPictureInPictureWindow() = default;
+  MockPictureInPictureWindow(const MockPictureInPictureWindow&) = delete;
+  MockPictureInPictureWindow& operator=(const MockPictureInPictureWindow&) =
+      delete;
+  ~MockPictureInPictureWindow() override = default;
+
+  bool is_tucking() const { return is_tucking_; }
+
+  // PictureInPictureWindow:
+  void SetForcedTucking(bool tuck) override { is_tucking_ = tuck; }
+
+ private:
+  bool is_tucking_ = false;
+};
+#endif  // !BUILDFLAG(IS_ANDROID)
+
 class PictureInPictureWindowManagerTest
     : public ChromeRenderViewHostTestHarness {
  public:
@@ -475,6 +496,103 @@
   }
 }
 
+TEST_F(PictureInPictureWindowManagerTest, CanForceTuckPictureInPicture) {
+  {
+    // Force-tucking before opening a picture-in-picture window should tuck it.
+    auto tuck = std::make_unique<ScopedTuckPictureInPicture>();
+    MockPictureInPictureWindow pip_window;
+
+    PictureInPictureWindowManager::GetInstance()->OnPictureInPictureWindowShown(
+        &pip_window);
+    EXPECT_TRUE(pip_window.is_tucking());
+
+    tuck.reset();
+    EXPECT_FALSE(pip_window.is_tucking());
+
+    PictureInPictureWindowManager::GetInstance()
+        ->OnPictureInPictureWindowHidden(&pip_window);
+  }
+
+  {
+    // Force-tucking after opening a picture-in-picture window should tuck it.
+    MockPictureInPictureWindow pip_window;
+    PictureInPictureWindowManager::GetInstance()->OnPictureInPictureWindowShown(
+        &pip_window);
+
+    EXPECT_FALSE(pip_window.is_tucking());
+    auto tuck = std::make_unique<ScopedTuckPictureInPicture>();
+    EXPECT_TRUE(pip_window.is_tucking());
+
+    tuck.reset();
+    EXPECT_FALSE(pip_window.is_tucking());
+
+    PictureInPictureWindowManager::GetInstance()
+        ->OnPictureInPictureWindowHidden(&pip_window);
+  }
+
+  {
+    MockPictureInPictureWindow pip_window;
+    {
+      ScopedTuckPictureInPicture tuck1;
+
+      {
+        // Multiple ScopedTuckPictureInPicture should still tuck
+        // picture-in-picture windows.
+        ScopedTuckPictureInPicture tuck2;
+
+        PictureInPictureWindowManager::GetInstance()
+            ->OnPictureInPictureWindowShown(&pip_window);
+        EXPECT_TRUE(pip_window.is_tucking());
+      }
+
+      // When one of them is destroyed but the other remains, it should still
+      // remain tucked.
+      EXPECT_TRUE(pip_window.is_tucking());
+    }
+
+    // Once both have been destroyed, picture-in-picture windows should be
+    // untucked.
+    EXPECT_FALSE(pip_window.is_tucking());
+
+    PictureInPictureWindowManager::GetInstance()
+        ->OnPictureInPictureWindowHidden(&pip_window);
+  }
+}
+
+TEST_F(PictureInPictureWindowManagerTest,
+       ShouldFileDialogTuckPictureInPicture) {
+  PictureInPictureWindowManager::GetInstance()->EnterDocumentPictureInPicture(
+      web_contents(), child_web_contents());
+
+  {
+    base::test::ScopedFeatureList feature_list;
+    feature_list.InitAndEnableFeature(media::kFileDialogsTuckPictureInPicture);
+
+    // With the feature enabled, file dialogs that aren't on a document
+    // picture-in-picture window should tuck picture-in-picture windows.
+    EXPECT_TRUE(PictureInPictureWindowManager::GetInstance()
+                    ->ShouldFileDialogTuckPictureInPicture(web_contents()));
+    EXPECT_FALSE(
+        PictureInPictureWindowManager::GetInstance()
+            ->ShouldFileDialogTuckPictureInPicture(child_web_contents()));
+  }
+
+  {
+    base::test::ScopedFeatureList feature_list;
+    feature_list.InitAndDisableFeature(media::kFileDialogsTuckPictureInPicture);
+
+    // With the feature disabled, no file dialogs should tuck
+    // picture-in-picture windows.
+    EXPECT_FALSE(PictureInPictureWindowManager::GetInstance()
+                     ->ShouldFileDialogTuckPictureInPicture(web_contents()));
+    EXPECT_FALSE(
+        PictureInPictureWindowManager::GetInstance()
+            ->ShouldFileDialogTuckPictureInPicture(child_web_contents()));
+  }
+
+  PictureInPictureWindowManager::GetInstance()->ExitPictureInPicture();
+}
+
 TEST_F(PictureInPictureWindowManagerTest,
        EnterAndCloseDocumentPip_NormalCloseDoesCommit) {
   base::SimpleTestTickClock test_clock;
diff --git a/chrome/browser/ui/views/overlay/video_overlay_window_views_unittest.cc b/chrome/browser/ui/views/overlay/video_overlay_window_views_unittest.cc
index 8b015e8..50e5fc34 100644
--- a/chrome/browser/ui/views/overlay/video_overlay_window_views_unittest.cc
+++ b/chrome/browser/ui/views/overlay/video_overlay_window_views_unittest.cc
@@ -15,6 +15,7 @@
 #include "chrome/browser/picture_in_picture/auto_pip_setting_overlay_view.h"
 #include "chrome/browser/picture_in_picture/picture_in_picture_occlusion_tracker.h"
 #include "chrome/browser/picture_in_picture/picture_in_picture_window_manager.h"
+#include "chrome/browser/picture_in_picture/scoped_tuck_picture_in_picture.h"
 #include "chrome/browser/ui/views/chrome_layout_provider.h"
 #include "chrome/browser/ui/views/overlay/back_to_tab_button.h"
 #include "chrome/browser/ui/views/overlay/close_image_button.h"
@@ -795,6 +796,45 @@
   ASSERT_EQ(nullptr, live_status);
 }
 
+TEST_F(VideoOverlayWindowViewsTest, CanBeTuckedToTheSideOfTheScreen) {
+  // Place the window on the left side of the screen.
+  SetDisplayWorkArea({0, 0, 2000, 2000});
+  overlay_window().SetBounds({{400, 400}, {500, 500}});
+
+  // If we tell it to force tucking, it should tuck to the left side of the
+  // screen.
+  overlay_window().SetForcedTucking(true);
+  EXPECT_LT(overlay_window().GetWindowBoundsInScreen().x(), 0);
+
+  // If we tell it to stop tucking, it should be put back in its original
+  // position.
+  overlay_window().SetForcedTucking(false);
+  EXPECT_EQ(overlay_window().GetWindowBoundsInScreen().x(), 400);
+}
+
+TEST_F(VideoOverlayWindowViewsTest, UntucksWhenReshownIfNecessary) {
+  // Place the window on the left side of the screen.
+  SetDisplayWorkArea({0, 0, 2000, 2000});
+  overlay_window().SetBounds({{400, 400}, {500, 500}});
+  overlay_window().ShowInactive();
+
+  // Start tucking via a ScopedTuckPictureInPicture.
+  auto scoped_tuck = std::make_unique<ScopedTuckPictureInPicture>();
+  EXPECT_LT(overlay_window().GetWindowBoundsInScreen().x(), 0);
+
+  // Hide ourselves. This will mean if tucking ends then the
+  // PictureInPictureWindowManager won't actually notify us.
+  overlay_window().Hide();
+
+  // End tucking.
+  scoped_tuck.reset();
+
+  // Show ourselves. We should check with the PictureInPictureWindowManager and
+  // realize we should no longer tuck.
+  overlay_window().ShowInactive();
+  EXPECT_EQ(overlay_window().GetWindowBoundsInScreen().x(), 400);
+}
+
 TEST_F(VideoOverlayWindowViewsTest,
        ReplayAndForward10SecondsNotDrawnWhen2024UIIsDisabled) {
   overlay_window().ForceControlsVisibleForTesting(true);
diff --git a/chrome/browser/ui/views/picture_in_picture/picture_in_picture_tucker_unittest.cc b/chrome/browser/ui/views/picture_in_picture/picture_in_picture_tucker_unittest.cc
new file mode 100644
index 0000000..372d502a
--- /dev/null
+++ b/chrome/browser/ui/views/picture_in_picture/picture_in_picture_tucker_unittest.cc
@@ -0,0 +1,144 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ui/views/picture_in_picture/picture_in_picture_tucker.h"
+
+#include "ui/display/display.h"
+#include "ui/display/screen.h"
+#include "ui/display/test/test_screen.h"
+#include "ui/views/test/views_test_base.h"
+#include "ui/views/widget/widget.h"
+
+class PictureInPictureTuckerTest : public views::ViewsTestBase {
+ public:
+  void SetUp() override {
+    display::Screen::SetScreenInstance(&test_screen_);
+    views::ViewsTestBase::SetUp();
+
+    widget_ = CreateTestWidget(views::Widget::InitParams::CLIENT_OWNS_WIDGET);
+    tucker_ = std::make_unique<PictureInPictureTucker>(*widget_.get());
+  }
+
+  void TearDown() override {
+    tucker_.reset();
+    widget_.reset();
+    views::ViewsTestBase::TearDown();
+    display::Screen::SetScreenInstance(nullptr);
+  }
+
+ protected:
+  display::test::TestScreen& test_screen() { return test_screen_; }
+  views::Widget* widget() { return widget_.get(); }
+  PictureInPictureTucker* tucker() { return tucker_.get(); }
+
+ private:
+  display::test::TestScreen test_screen_;
+  std::unique_ptr<views::Widget> widget_;
+  std::unique_ptr<PictureInPictureTucker> tucker_;
+};
+
+TEST_F(PictureInPictureTuckerTest, TucksTowardCloserEdge) {
+  const gfx::Rect work_area =
+      display::Screen::GetScreen()
+          ->GetDisplayNearestWindow(widget()->GetNativeWindow())
+          .work_area();
+
+  // Place the widget on the left side of the screen.
+  const gfx::Rect initial_leftside_bounds({work_area.x() + 20, 50}, {200, 200});
+  widget()->SetBounds(initial_leftside_bounds);
+  EXPECT_EQ(initial_leftside_bounds, widget()->GetWindowBoundsInScreen());
+
+  // Tuck the widget. This should tuck towards the left side.
+  tucker()->Tuck();
+  EXPECT_LT(widget()->GetWindowBoundsInScreen().x(),
+            initial_leftside_bounds.x());
+
+  // Untucking should place the widget back where it started.
+  tucker()->Untuck();
+  EXPECT_EQ(initial_leftside_bounds, widget()->GetWindowBoundsInScreen());
+
+  // Place the widget on the right side of the screen.
+  const gfx::Rect initial_rightside_bounds({work_area.right() - 220, 50},
+                                           {200, 200});
+  widget()->SetBounds(initial_rightside_bounds);
+  EXPECT_EQ(initial_rightside_bounds, widget()->GetWindowBoundsInScreen());
+
+  // Tuck the widget. This should tuck towards the right side.
+  tucker()->Tuck();
+  EXPECT_GT(widget()->GetWindowBoundsInScreen().x(),
+            initial_rightside_bounds.x());
+
+  // Untucking should place the widget back where it started.
+  tucker()->Untuck();
+  EXPECT_EQ(initial_rightside_bounds, widget()->GetWindowBoundsInScreen());
+}
+
+TEST_F(PictureInPictureTuckerTest,
+       RetuckingForResizeRemembersOriginalPosition) {
+  const gfx::Rect initial_bounds({20, 50}, {200, 200});
+  widget()->SetBounds(initial_bounds);
+  EXPECT_EQ(initial_bounds, widget()->GetWindowBoundsInScreen());
+
+  // Tuck the widget.
+  tucker()->Tuck();
+  ASSERT_NE(initial_bounds, widget()->GetWindowBoundsInScreen());
+
+  // Resize the widget and retuck it.
+  gfx::Size new_size = {300, 300};
+  widget()->SetSize(new_size);
+  tucker()->Tuck();
+
... (truncated)
Loading diff…

Original Bug Report

reported by [email protected]

Save As file dialog steal focus behind the PictureinPictureAPI save malicious file at arbitrary path

Steps to reproduce the problem

Enable Ask where to save each file before downloading in Downloads

  1. Open the poc.html file in the browser
  2. Click the button
  3. Click on Start file writing button
  4. Press the keys ctrl + v and keypress Enter key A file will be written

Problem Description

Save as file dialog can steal focus behind the Picture-in-Picture API and can save malicious at arbitrary file path it is possible only if a user has enabled Ask where to save each file before downloading in Downloads

it is possible to saving malicious file at an arbitrary file path. the save as file dialog stays behind the PiP window, completely obscured. Since the dialog still receives focus, it’s possible to trick the user into saving malicious file at any path

Additional Comments

Google Chrome 132.0.6834.46 (Official Build) beta (64-bit) (cohort: Beta) Revision c2f4d240db159da353deb230fea74b6a2f32533b-refs/branch-heads/6834@{#1946} OS Windows 10 Version 22H2 (Build 19045.5131)

Summary

Save As file dialog steal focus behind the PictureinPictureAPI save malicious file at arbitrary path

Custom Questions

Reporter credit:

Puf

Additional Data

Category: Security
Chrome Channel: Beta
Regression: N/A

View on issue tracker
Uploaded PoC files