Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Media
DescriptionInappropriate implementation in Media
ComponentMedia
Bug ClassLogic Error
Tracker397878997
Fix commitb2e3af66b3d3 (chromium/src) +231/-29
CISA KEVNot listed
CreditedKevin Joensen
Disclosed2025-09-30

Changed Functions

FunctionChangeNotes
for
chrome/browser/picture_in_picture/video_picture_in_picture_window_controller_browsertest.cc
modified
if
chrome/browser/ui/views/overlay/video_overlay_window_views.cc
modified

Files Changed

  • chrome/browser/picture_in_picture/video_picture_in_picture_window_controller_browsertest.cc
  • chrome/browser/ui/views/overlay/video_overlay_window_views.cc
From b2e3af66b3d34e1414ef87bee0a9a11319f688b1 Mon Sep 17 00:00:00 2001
From: Benjamin Keen <bkeen@google.com>
Date: Thu, 17 Jul 2025 14:43:09 -0700
Subject: [PATCH] Reland "Update video picture-in-picture window title animation"

This is a reland of commit 247cd023ec889ccb73f505a32099cece5d6f1afb

The failing test is fixed by forcing the title and scrim to not
visible, similar to how it is currently done for the controls.

Original change's description:
> Update video picture-in-picture window title animation
>
> This change updates the window animation by making the title (origin
> and favicon), together with the controls top scrim, display for 5
> seconds any time the picture-in-picture window is opened.
>
> Any interaction with the window causes the animation to revert back to
> the default implementation.
>
> Reasoning for this change available at:
> go/cme-video-pip-spoof-vulnerability-by-prompt-mimicking
>
> Bug: 397878997
> Change-Id: I2daf3d5915bc04d3a729a425e9a368e44d167a34
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6697443
> Reviewed-by: Fr <beaufort.francois@gmail.com>
> Commit-Queue: Benjamin Keen <bkeen@google.com>
> Cr-Commit-Position: refs/heads/main@{#1487129}

Bug: 397878997
Change-Id: Iae561fde0a4d9d9bece8882814056730a642e567
Include-Ci-Only-Tests: chromium.memory:Linux ASan LSan Tests (1)|browser_tests
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6759273
Commit-Queue: Benjamin Keen <bkeen@google.com>
Reviewed-by: Fr <beaufort.francois@gmail.com>
Cr-Commit-Position: refs/heads/main@{#1488536}
---

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.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>();
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 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:
Loading diff…

Original Bug Report

reported by ke...@gmail.com

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

  1. Visit https://com.apple.securityd.auth.system.service.scopefense.com/pocwin.html
  2. Click Get pwned (This could be any normal activity that triggers the transient activation)

PastedImg

  1. 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.

PastedImg

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)

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

View on issue tracker