[Extensions] Display window only when its bounds intersect the displays
Windows should only be displayed when they intersect the displays in a
meaningful manner (in this case, window should be visible at least 50%).
Otherwise, the window will be displayed in the current default.
This prevents for windows to be created or moved outside the displays,
and potentially been exploited.
Screencast: https://drive.google.com/file/d/1dTJdC1OtQek4n6ok0sOfccbEr8H0Ah3U/view?usp=sharing
Bug: 1301203
Change-Id: I306d837d12a79a99039e5336792a72cbe20ea57d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3536833
Reviewed-by: Mike Wasserman <msw@chromium.org>
Reviewed-by: Devlin Cronin <rdevlin.cronin@chromium.org>
Commit-Queue: Emilia Paz <emiliapaz@chromium.org>
Cr-Commit-Position: refs/heads/main@{#986515}
diff --git a/chrome/browser/extensions/api/tabs/tabs_api.cc b/chrome/browser/extensions/api/tabs/tabs_api.cc
index 0521e28..c09878e 100644
--- a/chrome/browser/extensions/api/tabs/tabs_api.cc
+++ b/chrome/browser/extensions/api/tabs/tabs_api.cc
@@ -108,6 +108,8 @@
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/base/models/list_selection_model.h"
#include "ui/base/ui_base_types.h"
+#include "ui/display/screen.h"
+#include "ui/gfx/geometry/rect.h"
#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "chrome/browser/ui/ash/window_pin_util.h"
@@ -425,6 +427,18 @@
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
}
+// Returns whether the given `bounds` intersect with at least 50% of all the
+// displays.
+bool WindowBoundsIntersectDisplays(const gfx::Rect& bounds) {
+ int intersect_area = 0;
+ for (const auto& display : display::Screen::GetScreen()->GetAllDisplays()) {
+ gfx::Rect display_bounds = display.bounds();
+ display_bounds.Intersect(bounds);
+ intersect_area += display_bounds.size().GetArea();
+ }
+ return intersect_area >= (bounds.size().GetArea() / 2);
+}
+
} // namespace
void ZoomModeToZoomSettings(ZoomController::ZoomMode zoom_mode,
@@ -711,18 +725,28 @@
WindowSizer::GetBrowserWindowBoundsAndShowState(
gfx::Rect(), nullptr, &window_bounds, &ignored_show_state);
- // Any part of the bounds can optionally be set by the caller.
- if (create_data->left)
+ // Update the window bounds if the bounds from the create parameters
+ // intersect the displays.
+ bool set_window_bounds = false;
+ if (create_data->left) {
window_bounds.set_x(*create_data->left);
-
- if (create_data->top)
+ set_window_bounds = true;
+ }
+ if (create_data->top) {
window_bounds.set_y(*create_data->top);
-
- if (create_data->width)
+ set_window_bounds = true;
+ }
+ if (create_data->width) {
window_bounds.set_width(*create_data->width);
-
- if (create_data->height)
+ set_window_bounds = true;
+ }
+ if (create_data->height) {
window_bounds.set_height(*create_data->height);
+ set_window_bounds = true;
+ }
+
+ if (set_window_bounds && !WindowBoundsIntersectDisplays(window_bounds))
+ return RespondNow(Error(tabs_constants::kInvalidWindowBoundsError));
if (create_data->focused)
focused = *create_data->focused;
@@ -868,37 +892,38 @@
// Before changing any of a window's state, validate the update parameters.
// This prevents Chrome from performing "half" an update.
+
+ // Update the window bounds if the bounds from the update parameters intersect
+ // the displays.
+ gfx::Rect window_bounds = browser->window()->IsMinimized()
+ ? browser->window()->GetRestoredBounds()
+ : browser->window()->GetBounds();
+ bool set_window_bounds = false;
+ if (params->update_info.left) {
+ window_bounds.set_x(*params->update_info.left);
+ set_window_bounds = true;
+ }
+ if (params->update_info.top) {
+ window_bounds.set_y(*params->update_info.top);
+ set_window_bounds = true;
+ }
+ if (params->update_info.width) {
+ window_bounds.set_width(*params->update_info.width);
+ set_window_bounds = true;
+ }
+ if (params->update_info.height) {
+ window_bounds.set_height(*params->update_info.height);
+ set_window_bounds = true;
+ }
+
+ if (set_window_bounds && !WindowBoundsIntersectDisplays(window_bounds))
+ return RespondNow(Error(tabs_constants::kInvalidWindowBoundsError));
+
ui::WindowShowState show_state =
ConvertToWindowShowState(params->update_info.state);
- gfx::Rect bounds = browser->window()->IsMinimized()
- ? browser->window()->GetRestoredBounds()
- : browser->window()->GetBounds();
- bool set_bounds = false;
-
- // Any part of the bounds can optionally be set by the caller.
- if (params->update_info.left) {
- bounds.set_x(*params->update_info.left);
- set_bounds = true;
- }
-
- if (params->update_info.top) {
- bounds.set_y(*params->update_info.top);
- set_bounds = true;
- }
-
- if (params->update_info.width) {
- bounds.set_width(*params->update_info.width);
- set_bounds = true;
- }
-
- if (params->update_info.height) {
- bounds.set_height(*params->update_info.height);
- set_bounds = true;
- }
-
- if (set_bounds && (show_state == ui::SHOW_STATE_MINIMIZED ||
- show_state == ui::SHOW_STATE_MAXIMIZED ||
- show_state == ui::SHOW_STATE_FULLSCREEN)) {
+ if (set_window_bounds && (show_state == ui::SHOW_STATE_MINIMIZED ||
+ show_state == ui::SHOW_STATE_MAXIMIZED ||
+ show_state == ui::SHOW_STATE_FULLSCREEN)) {
return RespondNow(Error(tabs_constants::kInvalidWindowStateError));
}
@@ -956,10 +981,10 @@
break;
}
- if (set_bounds) {
+ if (set_window_bounds) {
// TODO(varkha): Updating bounds during a drag can cause problems and a more
// general solution is needed. See http://crbug.com/251813 .
- browser->window()->SetBounds(bounds);
+ browser->window()->SetBounds(window_bounds);
}
if (params->update_info.focused) {
diff --git a/chrome/browser/extensions/api/tabs/tabs_constants.cc b/chrome/browser/extensions/api/tabs/tabs_constants.cc
index 3f068eb5..d76698f 100644
--- a/chrome/browser/extensions/api/tabs/tabs_constants.cc
+++ b/chrome/browser/extensions/api/tabs/tabs_constants.cc
@@ -108,6 +108,9 @@
const char kSupportedInWindowsOnlyError[] = "Supported in Windows only";
const char kInvalidWindowTypeError[] = "Invalid value for type";
const char kInvalidWindowStateError[] = "Invalid value for state";
+const char kInvalidWindowBoundsError[] =
+ "Invalid value for bounds. Bounds must be at least 50% within visible "
+ "screen space.";
const char kScreenshotsDisabled[] = "Taking screenshots has been disabled";
const char kScreenshotsDisabledByDlp[] =
"Administrator policy disables screen capture when confidential content is "
diff --git a/chrome/browser/extensions/api/tabs/tabs_constants.h b/chrome/browser/extensions/api/tabs/tabs_constants.h
index 49c6dbd5..9d580b8 100644
--- a/chrome/browser/extensions/api/tabs/tabs_constants.h
+++ b/chrome/browser/extensions/api/tabs/tabs_constants.h
@@ -103,6 +103,7 @@
extern const char kSupportedInWindowsOnlyError[];
extern const char kInvalidWindowTypeError[];
extern const char kInvalidWindowStateError[];
+extern const char kInvalidWindowBoundsError[];
extern const char kScreenshotsDisabled[];
extern const char kScreenshotsDisabledByDlp[];
extern const char kCannotUpdateMuteCaptured[];
diff --git a/chrome/browser/extensions/api/tabs/tabs_test.cc b/chrome/browser/extensions/api/tabs/tabs_test.cc
index f7917c18..50ef223 100644
--- a/chrome/browser/extensions/api/tabs/tabs_test.cc
+++ b/chrome/browser/extensions/api/tabs/tabs_test.cc
@@ -65,6 +65,8 @@
#include "pdf/buildflags.h"
#include "third_party/blink/public/common/page/page_zoom.h"
#include "ui/base/window_open_disposition.h"
+#include "ui/display/display.h"
+#include "ui/display/screen.h"
#include "ui/gfx/geometry/rect.h"
#if BUILDFLAG(IS_MAC)
@@ -754,6 +756,66 @@
keys::kInvalidWindowStateError));
}
+IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, InvalidUpdateWindowBounds) {
+ scoped_refptr<const Extension> extension(ExtensionBuilder("Test").Build());
+
+ // Get the display bounds so we can test whether the window intersects.
+ gfx::Rect displays;
+ for (const auto& display : display::Screen::GetScreen()->GetAllDisplays())
+ displays.Union(display.bounds());
+
+ int window_id = ExtensionTabUtil::GetWindowId(browser());
+ gfx::Rect window_bounds = browser()->window()->GetBounds();
+
+ static const char kArgsUpdateFunction[] = "[%u, {\"left\": %d, \"top\": %d}]";
+ // We use a small value to move the window outside or inside the bounds.
+ int window_offset = window_bounds.size().width() * 0.1;
+
+ {
+ // Window bounds that do not intersect with the display are not valid.
+ int window_left = displays.right() + window_offset;
+ int window_top = displays.bottom() + window_offset;
+ auto function = base::MakeRefCounted<WindowsUpdateFunction>();
+ function->set_extension(extension.get());
+ EXPECT_TRUE(base::MatchPattern(
+ utils::RunFunctionAndReturnError(
+ function.get(),
+ base::StringPrintf(kArgsUpdateFunction, window_id, window_left,
+ window_top),
+ browser()),
+ keys::kInvalidWindowBoundsError));
+ }
+
+ {
+ // Window bounds that intersect less than 50% with the display are not
+ // valid.
+ int window_left = displays.right() - window_offset;
+ int window_top = displays.bottom() - window_offset;
+ auto function = base::MakeRefCounted<WindowsUpdateFunction>();
+ function->set_extension(extension.get());
+ EXPECT_TRUE(base::MatchPattern(
+ utils::RunFunctionAndReturnError(
+ function.get(),
+ base::StringPrintf(kArgsUpdateFunction, window_id, window_left,
+ window_top),
+ browser()),
+ keys::kInvalidWindowBoundsError));
+ }
+
+ {
+ // Window bounds that intersect 50% or more with the display are valid.
+ int window_left = displays.right() - window_bounds.width() + window_offset;
+ int window_top = displays.bottom() - window_bounds.height() + window_offset;
+ auto function = base::MakeRefCounted<WindowsUpdateFunction>();
+ function->set_extension(extension.get());
+ EXPECT_TRUE(
+ utils::RunFunction(function.get(),
+ base::StringPrintf(kArgsUpdateFunction, window_id,
+ window_left, window_top),
+ browser(), api_test_utils::NONE));
+ }
+}
+
IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, UpdateDevToolsWindow) {
DevToolsWindow* devtools = DevToolsWindowTesting::OpenDevToolsWindowSync(
browser()->tab_strip_model()->GetWebContentsAt(0), false /* is_docked */);
@@ -890,6 +952,56 @@
keys::kInvalidWindowStateError));
}
+IN_PROC_BROWSER_TEST_F(ExtensionWindowCreateTest, ValidateCreateWindowBounds) {
+ // Get the display bounds so we can test whether the window intersects.
+ gfx::Rect displays;
+ for (const auto& display : display::Screen::GetScreen()->GetAllDisplays())
+ displays.Union(display.bounds());
+
+ static const char kArgsCreateFunction[] =
+ "[{\"left\": %d, \"top\": %d, \"width\": %d, \"height\": %d }]";
+ int window_width = 100;
+ int window_height = 100;
+ // We use a small value to move the window outside or inside the bounds.
+ int window_offset = 10;
+
+ {
+ // Window bounds that do not intersect with the display are not valid.
+ int window_left = displays.right() + window_offset;
+ int window_top = displays.bottom() + window_offset;
+ EXPECT_TRUE(
+ base::MatchPattern(RunCreateWindowExpectError(base::StringPrintf(
+ kArgsCreateFunction, window_left, window_top,
+ window_width, window_height)),
+ keys::kInvalidWindowBoundsError));
+ }
+
+ {
+ // Window bounds that intersect less than 50% with the display are not
+ // valid.
+ int window_left = displays.right() - window_offset;
+ int window_top = displays.bottom() - window_offset;
+ EXPECT_TRUE(
+ base::MatchPattern(RunCreateWindowExpectError(base::StringPrintf(
+ kArgsCreateFunction, window_left, window_top,
+ window_width, window_height)),
+ keys::kInvalidWindowBoundsError));
+ }
+
+ {
+ // Window bounds that intersect 50% or more with the display are valid.
+ int window_left = displays.right() - window_width + window_offset;
+ int window_top = displays.bottom() - window_height + window_offset;
+ auto function = base::MakeRefCounted<WindowsCreateFunction>();
+ function->set_extension(ExtensionBuilder("Test").Build().get());
+ EXPECT_TRUE(utils::RunFunction(
+ function.get(),
+ base::StringPrintf(kArgsCreateFunction, window_left, window_top,
+ window_width, window_height),
+ browser(), api_test_utils::NONE));
+ }
+}
+
IN_PROC_BROWSER_TEST_F(ExtensionWindowCreateTest, CreatePopupWindowFromWebUI) {
scoped_refptr<WindowsCreateFunction> function(new WindowsCreateFunction());
function->SetBrowserContextForTesting(browser()->profile());