CVE-2025-11208
Overview
Background
- `VideoOverlayWindowViews`
- the browser-process Views implementation of the video Picture-in-Picture (PiP) window, which draws the video plus overlay controls and, under
Use2024UI(), a title showing the page’sorigin_andfavicon_view_. - `controls_container_view_`
- the container whose layer opacity is faded in and out by
UpdateControlsVisibility. Its children are only visible while the controls are shown, for example on hover. - `title_view_` / `controls_top_scrim_view_`
- the new view that holds the origin text and favicon, plus the darkening scrim drawn behind it. The patch moves both out of
controls_container_view_so their visibility can be controlled separately. - `initial_title_hide_timer_`
- a new
base::OneShotTimerthat keeps the title and scrim visible forkTitleShowDuration(5 seconds) after the window is first shown.
Root Cause Analysis
Before the patch, under the 2024 UI, favicon_view_ and origin_ (and controls_top_scrim_view_) were added as children of controls_container_view. The only identification of the page that owns a PiP window was therefore visible only while the playback controls were faded in. UpdateControlsVisibility animated or set only GetControlsContainerView()->layer() opacity. When a PiP window opened without user hover, controls hidden, the page’s origin was never shown. The window could look like arbitrary attacker-chosen video content with no attribution, which fits the advisory’s “Inappropriate implementation in Media” (a UI/attribution weakness).
The fix reparents the origin and favicon into a separate title_view_ and moves the top scrim to view_holder_ directly. It also starts initial_title_hide_timer_ when the window is shown, and adds a title_is_visible computation, (wanted_visibility && Use2024UI()) || initial_title_hide_timer_.IsRunning(), so the origin is shown for 5 seconds on first display and whenever the controls are shown. The removed early returns in UpdateControlsVisibility were restructured into should_animate/else branches that drive the title, scrim and controls together. That is a refactor needed for the new view, not a change to any validation check.
kTitleShowDuration period.Attack Path
- Open PiP from a page
A web page puts a
<video>into Picture-in-Picture, which createsVideoOverlayWindowViewsin the browser process with the 2024 UI. - Controls stay hidden
Without user hover or a tap,
UpdateControlsVisibilitykeepscontrols_container_view_at opacity 0. Pre-patch, theorigin_andfavicon_view_children inherit that, so no origin is displayed. - Unattributed content The always-on-top PiP window shows attacker-chosen frames with no visible indication of which site produced them, which could support UI spoofing (inferred; the diff shows only the missing origin display, not a specific spoof).
- Post-patch behavior
The window-show path (hunk
@@ -2056, function not named in the hunk) now startsinitial_title_hide_timer_and callsUpdateControlsVisibility(false), sotitle_view_is shown for 5 seconds before fading out.
Impact Assessment
(inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/ui/views/overlay/video_overlay_window_views.cc |
modified | |
initial_title_hide_timer_for_testingchrome/browser/ui/views/overlay/video_overlay_window_views.cc |
modified | |
forchrome/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.ccchrome/browser/ui/views/overlay/video_overlay_window_views.ccchrome/browser/ui/views/overlay/video_overlay_window_views.hchrome/browser/ui/views/overlay/video_overlay_window_views_unittest.cc
Audit Directions
- Security indicators parented to auto-hiding containersIn other overlay or out-of-tab surfaces (Document PiP,
AutoPipSettingOverlayView, fullscreen bubbles, extension popups), check whether the origin or attribution UI is a child of a view whose opacity or visibility the page can influence or that hides by default. - Initial-show guaranteesCheck that every web-initiated always-on-top or detached window shows its origin for a minimum time on creation, and that this cannot be shortened by quickly triggering the code paths that stop
initial_title_hide_timer_or similar timers. - Opacity-only hidingLook for security UI hidden only by layer opacity while still laid out. Make sure paint order (the top scrim is now added before
controls_container_view_) and z-ordering cannot let page-controlled content cover the indicator.
Patch
diff --git a/chrome/browser/ui/views/overlay/video_overlay_window_views.cc b/chrome/browser/ui/views/overlay/video_overlay_window_views.cc
index 6c5cbed1..4e1a0bb 100644
--- a/chrome/browser/ui/views/overlay/video_overlay_window_views.cc
+++ b/chrome/browser/ui/views/overlay/video_overlay_window_views.cc
@@ -776,13 +776,17 @@
queued_controls_visibility_status_.reset();
}
-void VideoOverlayWindowViews::ForceControlsVisibleForTesting(bool visible) {
- force_controls_visible_ = visible;
- UpdateControlsVisibility(visible, /*should_animate=*/false);
+void VideoOverlayWindowViews::ForceControlsVisibleForTesting(
+ bool controls_visible,
+ std::optional<bool> title_and_scrim_visible) {
+ force_controls_visible_ = controls_visible;
+ force_title_and_scrim_visible_ = title_and_scrim_visible;
+ UpdateControlsVisibility(controls_visible, /*should_animate=*/false);
}
void VideoOverlayWindowViews::StopForcingControlsVisibleForTesting() {
force_controls_visible_.reset();
+ force_title_and_scrim_visible_.reset();
}
bool VideoOverlayWindowViews::AreControlsVisible() const {
@@ -823,25 +827,57 @@
const bool wanted_visibility =
!IsOverlayViewShown() && force_controls_visible_.value_or(is_visible);
- // If this shouldn't be animated, then cancel any existing animations and set
- // the opacity instantly.
- if (!should_animate) {
+ // If the controls are becoming visible, stop the initial hide timer.
+ if (wanted_visibility) {
+ initial_title_hide_timer_.Stop();
+ }
+
+ // The title and controls top scrim are visible if the controls are, or if we
+ // are in the initial "show" period.
+ const bool title_is_visible = force_title_and_scrim_visible_.has_value()
+ ? force_title_and_scrim_visible_.value()
+ : (wanted_visibility && Use2024UI()) ||
+ initial_title_hide_timer_.IsRunning();
+
+ if (should_animate) {
+ // Animate the title and top scrim.
+ if (title_is_visible != AreTitleAndScrimVisible()) {
+ title_fade_animation_ = std::make_unique<OverlayControlsFadeAnimation>(
+ *GetTitleView(), title_is_visible
+ ? OverlayControlsFadeAnimation::Type::kToShown
+ : OverlayControlsFadeAnimation::Type::kToHidden);
+ controls_top_scrim_fade_animation_ =
+ std::make_unique<OverlayControlsFadeAnimation>(
+ *GetControlsTopScrimView(),
+ title_is_visible ? OverlayControlsFadeAnimation::Type::kToShown
+ : OverlayControlsFadeAnimation::Type::kToHidden);
+
+ title_fade_animation_->Start();
+ controls_top_scrim_fade_animation_->Start();
+ }
+
+ // Animate the main controls.
+ if (wanted_visibility != AreControlsVisible()) {
+ fade_animation_ = std::make_unique<OverlayControlsFadeAnimation>(
+ *GetControlsContainerView(),
+ wanted_visibility ? OverlayControlsFadeAnimation::Type::kToShown
+ : OverlayControlsFadeAnimation::Type::kToHidden);
+ fade_animation_->Start();
+ }
+ } else {
+ // Instantly set the opacity for the title, top scrim and main controls.
+ title_fade_animation_.reset();
+ controls_top_scrim_fade_animation_.reset();
fade_animation_.reset();
+
+ if (Use2024UI()) {
+ GetTitleView()->layer()->SetOpacity(title_is_visible ? 1.0 : 0.0);
+ GetControlsTopScrimView()->layer()->SetOpacity(title_is_visible ? 1.0
+ : 0.0);
+ }
GetControlsContainerView()->layer()->SetOpacity(wanted_visibility ? 1.0
: 0.0);
- return;
}
-
- if (wanted_visibility == AreControlsVisible()) {
- return;
- }
-
- // Fade to the new visibility state.
- fade_animation_ = std::make_unique<OverlayControlsFadeAnimation>(
- *GetControlsContainerView(),
- wanted_visibility ? OverlayControlsFadeAnimation::Type::kToShown
- : OverlayControlsFadeAnimation::Type::kToHidden);
- fade_animation_->Start();
}
void VideoOverlayWindowViews::UpdateControlsBounds() {
@@ -871,6 +907,10 @@
}
}
+bool VideoOverlayWindowViews::AreTitleAndScrimVisibleForTesting() const {
+ return AreTitleAndScrimVisible();
+}
+
void VideoOverlayWindowViews::OnDisplayMetricsChanged(
const display::Display& display,
uint32_t changed_metrics) {
@@ -1014,6 +1054,14 @@
return controls_container_view_;
}
+views::View* VideoOverlayWindowViews::GetTitleView() const {
+ return title_view_;
+}
+
+views::View* VideoOverlayWindowViews::GetControlsTopScrimView() const {
+ return controls_top_scrim_view_;
+}
+
void VideoOverlayWindowViews::SetUpViews() {
// View that is displayed when video is hidden. ------------------------------
// Adding an extra pixel to width/height makes sure controls background cover
@@ -1022,6 +1070,7 @@
auto video_view = std::make_unique<views::View>();
auto controls_scrim_view = std::make_unique<ControlsBackgroundView>();
auto controls_container_view = std::make_unique<views::View>();
+ auto title_view = std::make_unique<views::View>();
auto close_controls_view = std::make_unique<CloseImageButton>(
base::BindRepeating(&VideoOverlayWindowViews::CloseAndPauseIfAvailable,
base::Unretained(this)));
@@ -1365,6 +1414,13 @@
back_to_tab_button->SetPaintToLayer(ui::LAYER_TEXTURED);
back_to_tab_button->layer()->SetFillsBoundsOpaquely(false);
back_to_tab_button->layer()->SetName("BackToTabControlsView");
+
+ // views::View that displays the window title. The window title consists of
+ // the origin and favicon. Always displayed together with the controls top
+ // scrim view.
+ title_view->SetPaintToLayer(ui::LAYER_TEXTURED);
+ title_view->layer()->SetFillsBoundsOpaquely(false);
+ title_view->layer()->SetName("TitleView");
} else {
// views::View that closes the window and focuses initiator tab. ----------
CHECK(back_to_tab_label_button);
@@ -1461,8 +1517,6 @@
controls_scrim_view_ =
controls_container_view->AddChildView(std::move(controls_scrim_view));
if (Use2024UI()) {
- controls_top_scrim_view_ = controls_container_view->AddChildView(
- std::move(controls_top_scrim_view));
controls_bottom_scrim_view_ = controls_container_view->AddChildView(
std::move(controls_bottom_scrim_view));
playback_controls_container_view_ = controls_container_view->AddChildView(
@@ -1482,11 +1536,10 @@
controls_container_view->AddChildView(std::move(close_controls_view));
if (Use2024UI()) {
// Initialize the favicon view with the default icon.
- favicon_view_ =
- controls_container_view->AddChildView(std::move(favicon_view));
+ favicon_view_ = title_view->AddChildView(std::move(favicon_view));
UpdateFavicon(gfx::ImageSkia());
- origin_ = controls_container_view->AddChildView(std::move(origin));
+ origin_ = title_view->AddChildView(std::move(origin));
minimize_button_ =
controls_container_view->AddChildView(std::move(minimize_button));
back_to_tab_button_ =
@@ -1545,8 +1598,16 @@
resize_handle_view_ =
controls_container_view->AddChildView(std::move(resize_handle_view));
#endif
+ // The top scrim is added before the other views so it is drawn behind them.
+ if (Use2024UI()) {
+ controls_top_scrim_view_ =
+ AddChildView(&view_holder_, std::move(controls_top_scrim_view));
+ }
controls_container_view_ =
AddChildView(&view_holder_, std::move(controls_container_view));
+ if (Use2024UI()) {
+ title_view_ = AddChildView(&view_holder_, std::move(title_view));
+ }
}
void VideoOverlayWindowViews::OnRootViewReady() {
@@ -2056,6 +2117,20 @@
SetBounds(CalculateAndUpdateWindowBounds());
}
+ if (Use2024UI()) {
+ // When the window is first shown, make the title and top controls
+ // visible for a few seconds.
+ initial_title_hide_timer_.Start(
+ FROM_HERE, kTitleShowDuration,
+ // base::Unretained() is safe since the callback will not be called
+ // after `initial_title_hide_timer_` is destroyed, and it is owned by
+ // this object.
+ base::BindOnce(&VideoOverlayWindowViews::OnInitialTitleTimerFired,
+ base::Unretained(this)));
+ // The controls are not visible, but the title should be.
+ UpdateControlsVisibility(false);
+ }
+
// If this is not the first time the window is shown, this will be a no-op.
has_been_shown_ = true;
@@ -2597,6 +2672,20 @@
return video_view_->layer();
}
+views::View* VideoOverlayWindowViews::title_view_for_testing() const {
+ return title_view_;
+}
+
+views::View* VideoOverlayWindowViews::controls_top_scrim_view_for_testing()
+ const {
+ return controls_top_scrim_view_;
+}
+
+base::OneShotTimer&
+VideoOverlayWindowViews::initial_title_hide_timer_for_testing() {
+ return initial_title_hide_timer_;
+}
+
const viz::FrameSinkId* VideoOverlayWindowViews::GetCurrentFrameSinkId() const {
if (auto* surface = video_view_->layer()->GetSurfaceId()) {
return &surface->frame_sink_id();
@@ -2690,3 +2779,30 @@
favicon_view_->SetImage(ui::ImageModel::FromImageSkia(favicon));
}
}
+
+void VideoOverlayWindowViews::OnInitialTitleTimerFired() {
+ UpdateControlsVisibility(false);
+}
+
+bool VideoOverlayWindowViews::AreTitleAndScrimVisible() const {
+ if (!Use2024UI()) {
+ return false;
+ }
+
+ if (title_fade_animation_) {
+ // The title and scrim are animated together, so their animations should
+ // either both exist or both not exist.
+ DCHECK(controls_top_scrim_fade_animation_);
+ DCHECK_EQ(title_fade_animation_->type(),
+ controls_top_scrim_fade_animation_->type());
+ return (title_fade_animation_->type() ==
+ OverlayControlsFadeAnimation::Type::kToShown);
+ }
+
+ // If no animation is active, check the opacity of the layers. They should
+ // also be in sync.
+ DCHECK(!controls_top_scrim_fade_animation_);
+ DCHECK_EQ(GetTitleView()->layer()->opacity(),
+ GetControlsTopScrimView()->layer()->opacity());
+ return GetTitleView()->layer()->opacity() > 0;
+}
diff --git a/chrome/browser/ui/views/overlay/video_overlay_window_views.h b/chrome/browser/ui/views/overlay/video_overlay_window_views.h
index c167d59..6a212596 100644
--- a/chrome/browser/ui/views/overlay/video_overlay_window_views.h
+++ b/chrome/browser/ui/views/overlay/video_overlay_window_views.h
@@ -76,6 +76,9 @@
static constexpr base::TimeDelta kControlHideDelayAfterMove =
base::Milliseconds(100);
+ // The amount of time to display the title and top controls scrim.
+ static constexpr base::TimeDelta kTitleShowDuration = base::Seconds(5);
+
// VideoOverlayWindow:
void Close() override;
void ShowInactive() override;
@@ -151,6 +154,8 @@
content::PictureInPictureWindowController* GetController() const;
views::View* GetWindowBackgroundView() const;
views::View* GetControlsContainerView() const;
+ views::View* GetTitleView() const;
+ views::View* GetControlsTopScrimView() const;
gfx::Size& GetNaturalSize();
@@ -216,8 +221,13 @@
views::View* window_background_view_for_testing() const {
return window_background_view_;
}
+ views::View* title_view_for_testing() const;
+ views::View* controls_top_scrim_view_for_testing() const;
+ base::OneShotTimer& initial_title_hide_timer_for_testing();
- void ForceControlsVisibleForTesting(bool visible);
+ void ForceControlsVisibleForTesting(
+ bool controls_visible,
+ std::optional<bool> title_and_scrim_visible = std::nullopt);
void StopForcingControlsVisibleForTesting();
void set_overlay_view_cb_for_testing(GetOverlayViewCb get_overlay_view_cb) {
@@ -242,6 +252,8 @@
void FinishTuckAnimationForTesting();
+ bool AreTitleAndScrimVisibleForTesting() const;
+
protected:
explicit VideoOverlayWindowViews(
content::VideoPictureInPictureWindowController* controller);
@@ -342,6 +354,12 @@
void OnFaviconReceived(const SkBitmap& image);
void UpdateFavicon(const gfx::ImageSkia& favicon);
+ // Called when the timer to initially show the title view fires.
+ void OnInitialTitleTimerFired();
+
+ // Returns true if the title and top scrim are visible.
+ bool AreTitleAndScrimVisible() const;
+
// Not owned; |controller_| owns |this|.
raw_ptr<content::VideoPictureInPictureWindowController> controller_;
@@ -372,6 +390,10 @@
// Automatically hides the controls a few seconds after user tap gesture.
base::RetainingOneShotTimer hide_controls_timer_;
+ // Automatically hides the title view a few seconds after the window is first
+ // shown.
+ base::OneShotTimer initial_title_hide_timer_;
+
// Used to track movement of the window. The mouse movement and the window
// movement can cause the overlay to flicker, because mouse movement shows
// the overlay while the window movement hides the overlay. A timer is used
@@ -393,6 +415,11 @@
// ForceControlsVisibleForTesting().
std::optional<bool> force_controls_visible_;
+ // If set, title and scrim will always either be shown or hidden, instead of
+ // showing and hiding automatically. Only used for testing via
+ // ForceControlsVisibleForTesting().
+ std::optional<bool> force_title_and_scrim_visible_;
+
// Views to be shown. The views are first temporarily owned by view_holder_,
// then passed to this widget's ContentsView which takes ownership.
raw_ptr<views::View> window_background_view_ = nullptr;
@@ -429,6 +456,7 @@
raw_ptr<SimpleOverlayWindowImageButton> live_caption_button_ = nullptr;
raw_ptr<OverlayWindowLiveCaptionDialog> live_caption_dialog_ = nullptr;
raw_ptr<AutoPipSettingOverlayView> overlay_view_ = nullptr;
+ raw_ptr<views::View> title_view_ = nullptr;
#if BUILDFLAG(IS_CHROMEOS)
// Generates a nine patch layer painted with a highlight border for ChromeOS
@@ -488,6 +516,12 @@
// Used for when the controls change visibility.
std::unique_ptr<OverlayControlsFadeAnimation> fade_animation_;
+ // Used for when the title changes visibility. The title and controls top
+ // scrim must be animated together.
+ std::unique_ptr<OverlayControlsFadeAnimation> title_fade_animation_;
+ std::unique_ptr<OverlayControlsFadeAnimation>
+ controls_top_scrim_fade_animation_;
+
// Callback to get / create an overlay view. This is a callback to let tests
// provide alternate implementations.
GetOverlayViewCb get_overlay_view_cb_;
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 60d7c34..519b5764 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
@@ -442,8 +442,7 @@
return gfx::PNGCodec::Decode(png_data.value());
}
- void TakeOverlayWindowScreenshot(const gfx::Size& window_size,
- bool controls_visible) {
+ void TakeOverlayWindowScreenshot(const gfx::Size& window_size) {
for (int i = 0; i < 2; ++i) {
WidgetSizeChangeWaiter bounds_change_waiter(GetOverlayWindow(),
window_size);
@@ -454,8 +453,9 @@
const auto initial_count = bounds_change_waiter.bounds_change_count();
// Make sure native widget events won't unexpectedly hide or show the
- // controls.
- GetOverlayWindow()->ForceControlsVisibleForTesting(controls_visible);
+ // controls nor the title and scrim.
+ GetOverlayWindow()->ForceControlsVisibleForTesting(
+ /*controls_visible=*/false, /*title_and_scrim_visible=*/false);
ui::Layer* const layer = GetOverlayWindow()->GetRootView()->layer();
layer->CompleteAllAnimations();
@@ -540,7 +540,7 @@
ASSERT_EQ(true, EvalJs(active_web_contents, "play();"));
- TakeOverlayWindowScreenshot({402, 268}, /*controls_visible=*/false);
+ TakeOverlayWindowScreenshot({402, 268});
base::FilePath expected_image_path =
GetFilePath(FILE_PATH_LITERAL("pixel_expected_video_play.png"));
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 bdb3d498..653d71c 100644
... (file diff truncated)
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 60d7c34..519b5764 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
@@ -442,8 +442,7 @@
return gfx::PNGCodec::Decode(png_data.value());
}
- void TakeOverlayWindowScreenshot(const gfx::Size& window_size,
- bool controls_visible) {
+ void TakeOverlayWindowScreenshot(const gfx::Size& window_size) {
for (int i = 0; i < 2; ++i) {
WidgetSizeChangeWaiter bounds_change_waiter(GetOverlayWindow(),
window_size);
@@ -454,8 +453,9 @@
const auto initial_count = bounds_change_waiter.bounds_change_count();
// Make sure native widget events won't unexpectedly hide or show the
- // controls.
- GetOverlayWindow()->ForceControlsVisibleForTesting(controls_visible);
+ // controls nor the title and scrim.
+ GetOverlayWindow()->ForceControlsVisibleForTesting(
+ /*controls_visible=*/false, /*title_and_scrim_visible=*/false);
ui::Layer* const layer = GetOverlayWindow()->GetRootView()->layer();
layer->CompleteAllAnimations();
@@ -540,7 +540,7 @@
ASSERT_EQ(true, EvalJs(active_web_contents, "play();"));
- TakeOverlayWindowScreenshot({402, 268}, /*controls_visible=*/false);
+ TakeOverlayWindowScreenshot({402, 268});
base::FilePath expected_image_path =
GetFilePath(FILE_PATH_LITERAL("pixel_expected_video_play.png"));
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 bdb3d498..653d71c 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
@@ -1432,6 +1432,58 @@
EXPECT_FALSE(live_caption_dialog->IsDrawn());
}
+TEST_F(VideoOverlayWindowViewsWith2024UITest, InitialTitleAndScrimVisibility) {
+ overlay_window().ForceControlsVisibleForTesting(false);
+ overlay_window().ShowInactive();
+ WaitForLayout();
+
+ // The initial title hide timer should be running.
+ EXPECT_TRUE(
+ overlay_window().initial_title_hide_timer_for_testing().IsRunning());
+
+ // Title and scrim should be visible.
+ EXPECT_TRUE(overlay_window().AreTitleAndScrimVisibleForTesting());
+}
+
+TEST_F(VideoOverlayWindowViewsWith2024UITest, TitleAndScrimHideAfterTimer) {
+ overlay_window().ShowInactive();
+ WaitForLayout();
+
+ // Fast forward time to fire the timer.
+ task_environment()->FastForwardBy(
+ VideoOverlayWindowViews::kTitleShowDuration);
+
+ // Timer should not be running anymore.
+ EXPECT_FALSE(
+ overlay_window().initial_title_hide_timer_for_testing().IsRunning());
+
+ // Title and scrim should now be animating to hidden.
+ EXPECT_FALSE(overlay_window().AreTitleAndScrimVisibleForTesting());
+ EXPECT_FALSE(overlay_window().AreControlsVisible());
+}
+
+TEST_F(VideoOverlayWindowViewsWith2024UITest, MouseHoverShowsAllControls) {
+ overlay_window().ShowInactive();
+ WaitForLayout();
+
+ // Move mouse over the window.
+ const auto controls_top_scrim_bounds =
+ overlay_window().controls_top_scrim_view_for_testing()->bounds();
+ const gfx::Point moved_location(controls_top_scrim_bounds.CenterPoint());
+ ui::MouseEvent moved_event(ui::EventType::kMouseMoved, moved_location,
+ moved_location, ui::EventTimeForNow(), ui::EF_NONE,
+ ui::EF_NONE);
+ overlay_window().OnMouseEvent(&moved_event);
+
+ // Timer should not be running anymore.
+ EXPECT_FALSE(
+ overlay_window().initial_title_hide_timer_for_testing().IsRunning());
+
+ // All controls should now be visible or animating to visible.
+ EXPECT_TRUE(overlay_window().AreTitleAndScrimVisibleForTesting());
+ EXPECT_TRUE(overlay_window().AreControlsVisible());
+}
+
class VideoOverlayWindowWithShowAnimationTest
: public VideoOverlayWindowViewsTest {
public:
Original Bug Report
Video Document In Document spoof login box
Report description
Video Document In Document spoof login box
Bug location
Where do you want to report your vulnerability?
Chrome VRP – Report security issues affecting the Chrome browser. See program rules
Which URL (or repository) have you found the vulnerability in?
Google Chrome
The problem
Please describe the technical details of the vulnerability
NOTE: Report is attached which will show images
Description
A vulnerability in Google Chrome’s Picture-in-Picture (PiP) feature allows an attacker to display a persistent, fake authentication prompt—mimicking trusted interfaces like Apple’s login or Okta’s password box. This is done by having a video that looks exactly like a login box which does not show the origin. The main window will pickup the keystrokes. Although PiP requires user activation, the nearly 5-second timeout on transient activation (https://developer.mozilla.org/en-US/docs/Glossary/Transient_activation) can be exploited to show the deceptive popup while the user is on a completely different workspace. You can use the blur event to detect this.
https://developer.mozilla.org/en-US/docs/Web/API/DocumentPictureInPicture/requestWindow
System Details
Version tested: Version 131.0.6778.265 (Official Build) (arm64)
OSX: Sonoma 14.5
Macbook Pro M3
Proof of concept
- Visit https://com.apple.securityd.auth.system.service.scopefense.com/pocwin.html
- Click Get pwned (This could be any normal activity that triggers the transient activation)
- The victim is likely to trust the prompt—believing it to be a legitimate out-of-browser request—and may enter credentials that can be stolen.
When they enter the password the video src tag will be switched everytime so it looks like a correct password entry. Since the video tag listens for enter tag to close the box, it will close when people click enter. The domain name does not matter as it is never shown
Video of the attack is provided here: https://drive.proton.me/urls/KASADP51V8#Ma54GF0Ida1b
Impact
Users typically recognize legitimacy through persistent, out-of-browser popups. This vulnerability undermines that assurance, enabling attackers to create convincing fake login interfaces (such as OKTA, Google Auth, or 2FA prompts) with minimal visible indicators of their true origin.
Remediation
Origin needs to be shown on video boxes as they can resemble web pages. Furthermore the transient keys should switch to sticky keys.
Please briefly explain who can exploit the vulnerability, and what they gain when doing so
Everyone can exploit it and they can pop a fake window that looks exactly like common login boxes, which could trigger the user to enter credentials
The cause
What version of Chrome have you found the security issue in?
Version 131.0.6778.265 (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?
Kevin Joensen
- https://bughunters.google.com/about/rules/5745167867576320/chrome-vulnerability-reward-program-rules
- https://com.apple.securityd.auth.system.service.scopefense.com/pocwin.html
- https://developer.mozilla.org/en-US/docs/Glossary/Transient_activation
- https://developer.mozilla.org/en-US/docs/Web/API/DocumentPictureInPicture/requestWindow
- https://drive.proton.me/urls/KASADP51V8#Ma54GF0Ida1b