CVE-2026-106420
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
forchrome/browser/extensions/api/tabs/tabs_test.cc |
modified | |
IN_PROC_BROWSER_TEST_Fchrome/browser/extensions/api/tabs/tabs_test.cc |
modified |
Files Changed
chrome/browser/extensions/api/tabs/tabs_test.ccchrome/browser/extensions/api/tabs/tabs_api.cc
Patch
From 42d8e82efd1a5c6a13af3bb40bf954c1c0f7518e Mon Sep 17 00:00:00 2001
From: Andrew Paseltiner <apaseltiner@chromium.org>
Date: Thu, 10 Sep 2026 10:43:33 -0700
Subject: [PATCH] Fix window bounds intersection check for small window sizes
WindowBoundsIntersectDisplays() requires requested window bounds to
intersect displays by at least 50% of their area. Previously, the
threshold was calculated using integer division by two, which truncated
to zero for areas of 0 or 1. As a result, non-intersecting bounds with
empty or tiny areas were treated as valid and accepted.
This change:
- Explicitly rejects empty bounds.
- Caches the validated window area to avoid recomputing it.
- Uses multiplication (2 * intersect_area >= bounds_area) rather than
division to ensure the 50% threshold calculation is exact for all
sizes.
- Widens the intersection accumulator to int64_t to prevent arithmetic
overflow across displays.
- Keeps window creation specifying only a position supported on
platforms where default bounds have not yet been initialized.
TAG=agy
CONV=aa700909-149a-4cce-93e8-4743710a6226
Fixed: 477327257
Change-Id: I153ce57f28505f5aa45bd4cef710f15d5138228c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8379586
Commit-Queue: Andrew Paseltiner <apaseltiner@chromium.org>
Reviewed-by: Tim <tjudkins@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1695499}
---
diff --git a/chrome/browser/extensions/api/tabs/tabs_api.cc b/chrome/browser/extensions/api/tabs/tabs_api.cc
index 2bd7e200..6d67307 100644
--- a/chrome/browser/extensions/api/tabs/tabs_api.cc
+++ b/chrome/browser/extensions/api/tabs/tabs_api.cc
@@ -839,13 +839,22 @@
return false;
}
- int intersect_area = 0;
+ // An empty rect cannot intersect any display.
+ if (bounds.IsEmpty()) {
+ return false;
+ }
+
+ const int bounds_area = checked_area.ValueOrDie();
+
+ int64_t intersect_area = 0;
for (const auto& display : display::Screen::Get()->GetAllDisplays()) {
gfx::Rect display_bounds = display.bounds();
display_bounds.Intersect(bounds);
intersect_area += display_bounds.size().GetArea();
}
- return intersect_area >= (bounds.size().GetArea() / 2);
+ // Compare using multiplication rather than division, which would truncate
+ // to zero for areas smaller than two.
+ return 2 * intersect_area >= bounds_area;
}
} // namespace tabs_internal
@@ -1511,8 +1520,12 @@
window_bounds.AdjustToFit(display.bounds());
}
- // Immediately fail if the window bounds don't intersect the displays.
- if ((set_window_position || set_window_size) &&
+ // Immediately fail if the window bounds don't intersect the displays. If
+ // only a position was specified and the default bounds have not been
+ // initialized (see above), there is no size to validate against yet.
+ const bool has_bounds_to_validate =
+ set_window_size || (set_window_position && !window_bounds.IsEmpty());
+ if (has_bounds_to_validate &&
!tabs_internal::WindowBoundsIntersectDisplays(window_bounds)) {
return tabs_constants::kInvalidWindowBoundsError;
}
diff --git a/chrome/browser/extensions/api/tabs/tabs_test.cc b/chrome/browser/extensions/api/tabs/tabs_test.cc
index 282aa75..723621d8 100644
--- a/chrome/browser/extensions/api/tabs/tabs_test.cc
+++ b/chrome/browser/extensions/api/tabs/tabs_test.cc
@@ -1061,6 +1061,52 @@
}
}
+IN_PROC_BROWSER_TEST_F(ExtensionTabsTest,
+ InvalidUpdateWindowBoundsWithSmallSizes) {
+ scoped_refptr<const Extension> extension(ExtensionBuilder("Test").Build());
+
+ // Get the display bounds so we can position the window outside of them.
+ gfx::Rect displays;
+ for (const auto& display : display::Screen::Get()->GetAllDisplays()) {
+ displays.Union(display.bounds());
+ }
+
+ int window_id = ExtensionTabUtil::GetWindowId(browser_window_interface());
+ int window_left = displays.right() + 10;
+ int window_top = displays.bottom() + 10;
+
+ static const char kArgsUpdateFunction[] =
+ "[%u, {\"left\": %d, \"top\": %d, \"width\": %d, \"height\": %d}]";
+
+ // Windows with an empty or tiny size cannot be moved outside the displays.
+ constexpr struct {
+ int width;
+ int height;
+ } kSizes[] = {{0, 500}, {500, 0}, {0, 0}, {1, 1}};
+ for (const auto& size : kSizes) {
+ 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, size.width, size.height),
+ profile()),
+ keys::kInvalidWindowBoundsError))
+ << "size: " << size.width << "x" << size.height;
+ }
+
+ // Resizing a window to an empty size is not valid regardless of its
+ // position.
+ auto function = base::MakeRefCounted<WindowsUpdateFunction>();
+ function->set_extension(extension.get());
+ EXPECT_TRUE(base::MatchPattern(
+ utils::RunFunctionAndReturnError(
+ function.get(), base::StringPrintf("[%u, {\"width\": 0}]", window_id),
+ profile()),
+ keys::kInvalidWindowBoundsError));
+}
+
// On Android this fails when Run() calls BaseWindow::CanResize() returns false
// due to default Android Browser Tests not having free-form windows.
#if !BUILDFLAG(IS_ANDROID)
@@ -1095,6 +1141,26 @@
profile(), api_test_utils::FunctionMode::kNone));
}
}
+
+IN_PROC_BROWSER_TEST_F(ExtensionTabsTest,
+ UpdatingWindowBoundsSucceedsForSmallOnScreenBounds) {
+ scoped_refptr<const Extension> extension(ExtensionBuilder("Test").Build());
+
+ int window_id = ExtensionTabUtil::GetWindowId(browser_window_interface());
+
+ // A tiny window is valid as long as it is fully within a display.
+ const gfx::Point center =
+ display::Screen::Get()->GetPrimaryDisplay().bounds().CenterPoint();
+ static const char kArgsUpdateFunction[] =
+ "[%u, {\"left\": %d, \"top\": %d, \"width\": 1, \"height\": 1}]";
+ auto function = base::MakeRefCounted<WindowsUpdateFunction>();
+ function->set_extension(extension.get());
+ EXPECT_TRUE(
+ utils::RunFunction(function.get(),
+ base::StringPrintf(kArgsUpdateFunction, window_id,
+ center.x(), center.y()),
+ profile(), api_test_utils::FunctionMode::kNone));
+}
#endif // !BUILDFLAG(IS_ANDROID)
#if BUILDFLAG(IS_ANDROID)
@@ -1395,6 +1461,52 @@
}
}
+IN_PROC_BROWSER_TEST_F(ExtensionWindowCreateTest,
+ ValidateCreateWindowBoundsWithSmallSizes) {
+ // Get the display bounds so we can position the window outside of them.
+ gfx::Rect displays;
+ for (const auto& display : display::Screen::Get()->GetAllDisplays()) {
+ displays.Union(display.bounds());
+ }
+
+ static const char kArgsCreateFunction[] =
+ "[{\"left\": %d, \"top\": %d, \"width\": %d, \"height\": %d }]";
+ int window_left = displays.right() + 10;
+ int window_top = displays.bottom() + 10;
+
+ // Windows with an empty or tiny size cannot be created outside the
+ // displays.
+ constexpr struct {
+ int width;
+ int height;
+ } kSizes[] = {{0, 100}, {100, 0}, {0, 0}, {1, 1}};
+ for (const auto& size : kSizes) {
+ EXPECT_TRUE(base::MatchPattern(
+ RunCreateWindowExpectError(base::StringPrintf(kArgsCreateFunction,
+ window_left, window_top,
+ size.width, size.height)),
+ keys::kInvalidWindowBoundsError))
+ << "size: " << size.width << "x" << size.height;
+ }
+
+ // Bounds with an empty size are not valid regardless of position.
+ EXPECT_TRUE(base::MatchPattern(
+ RunCreateWindowExpectError("[{\"width\": 0, \"height\": 0 }]"),
+ keys::kInvalidWindowBoundsError));
+
+ {
+ // A tiny window is valid as long as it is fully within a display.
+ const gfx::Point center =
+ display::Screen::Get()->GetPrimaryDisplay().bounds().CenterPoint();
+ auto function = base::MakeRefCounted<WindowsCreateFunction>();
+ function->set_extension(ExtensionBuilder("Test").Build().get());
+ EXPECT_TRUE(utils::RunFunction(
+ function.get(),
+ base::StringPrintf(kArgsCreateFunction, center.x(), center.y(), 1, 1),
+ profile(), api_test_utils::FunctionMode::kNone));
+ }
+}
+
IN_PROC_BROWSER_TEST_F(ExtensionWindowCreateTest, CreatePopupWindowFromWebUI) {
auto function = base::MakeRefCounted<WindowsCreateFunction>();
function->SetBrowserContextForTesting(profile());
Regression Test / PoC
diff --git a/chrome/browser/extensions/api/tabs/tabs_test.cc b/chrome/browser/extensions/api/tabs/tabs_test.cc
index 282aa75..723621d8 100644
--- a/chrome/browser/extensions/api/tabs/tabs_test.cc
+++ b/chrome/browser/extensions/api/tabs/tabs_test.cc
@@ -1061,6 +1061,52 @@
}
}
+IN_PROC_BROWSER_TEST_F(ExtensionTabsTest,
+ InvalidUpdateWindowBoundsWithSmallSizes) {
+ scoped_refptr<const Extension> extension(ExtensionBuilder("Test").Build());
+
+ // Get the display bounds so we can position the window outside of them.
+ gfx::Rect displays;
+ for (const auto& display : display::Screen::Get()->GetAllDisplays()) {
+ displays.Union(display.bounds());
+ }
+
+ int window_id = ExtensionTabUtil::GetWindowId(browser_window_interface());
+ int window_left = displays.right() + 10;
+ int window_top = displays.bottom() + 10;
+
+ static const char kArgsUpdateFunction[] =
+ "[%u, {\"left\": %d, \"top\": %d, \"width\": %d, \"height\": %d}]";
+
+ // Windows with an empty or tiny size cannot be moved outside the displays.
+ constexpr struct {
+ int width;
+ int height;
+ } kSizes[] = {{0, 500}, {500, 0}, {0, 0}, {1, 1}};
+ for (const auto& size : kSizes) {
+ 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, size.width, size.height),
+ profile()),
+ keys::kInvalidWindowBoundsError))
+ << "size: " << size.width << "x" << size.height;
+ }
+
+ // Resizing a window to an empty size is not valid regardless of its
+ // position.
+ auto function = base::MakeRefCounted<WindowsUpdateFunction>();
+ function->set_extension(extension.get());
+ EXPECT_TRUE(base::MatchPattern(
+ utils::RunFunctionAndReturnError(
+ function.get(), base::StringPrintf("[%u, {\"width\": 0}]", window_id),
+ profile()),
+ keys::kInvalidWindowBoundsError));
+}
+
// On Android this fails when Run() calls BaseWindow::CanResize() returns false
// due to default Android Browser Tests not having free-form windows.
#if !BUILDFLAG(IS_ANDROID)
@@ -1095,6 +1141,26 @@
profile(), api_test_utils::FunctionMode::kNone));
}
}
+
+IN_PROC_BROWSER_TEST_F(ExtensionTabsTest,
+ UpdatingWindowBoundsSucceedsForSmallOnScreenBounds) {
+ scoped_refptr<const Extension> extension(ExtensionBuilder("Test").Build());
+
+ int window_id = ExtensionTabUtil::GetWindowId(browser_window_interface());
+
+ // A tiny window is valid as long as it is fully within a display.
+ const gfx::Point center =
+ display::Screen::Get()->GetPrimaryDisplay().bounds().CenterPoint();
+ static const char kArgsUpdateFunction[] =
+ "[%u, {\"left\": %d, \"top\": %d, \"width\": 1, \"height\": 1}]";
+ auto function = base::MakeRefCounted<WindowsUpdateFunction>();
+ function->set_extension(extension.get());
+ EXPECT_TRUE(
+ utils::RunFunction(function.get(),
+ base::StringPrintf(kArgsUpdateFunction, window_id,
+ center.x(), center.y()),
+ profile(), api_test_utils::FunctionMode::kNone));
+}
#endif // !BUILDFLAG(IS_ANDROID)
#if BUILDFLAG(IS_ANDROID)
@@ -1395,6 +1461,52 @@
}
}
+IN_PROC_BROWSER_TEST_F(ExtensionWindowCreateTest,
+ ValidateCreateWindowBoundsWithSmallSizes) {
+ // Get the display bounds so we can position the window outside of them.
+ gfx::Rect displays;
+ for (const auto& display : display::Screen::Get()->GetAllDisplays()) {
+ displays.Union(display.bounds());
+ }
+
+ static const char kArgsCreateFunction[] =
+ "[{\"left\": %d, \"top\": %d, \"width\": %d, \"height\": %d }]";
+ int window_left = displays.right() + 10;
+ int window_top = displays.bottom() + 10;
+
+ // Windows with an empty or tiny size cannot be created outside the
+ // displays.
+ constexpr struct {
+ int width;
+ int height;
+ } kSizes[] = {{0, 100}, {100, 0}, {0, 0}, {1, 1}};
+ for (const auto& size : kSizes) {
+ EXPECT_TRUE(base::MatchPattern(
+ RunCreateWindowExpectError(base::StringPrintf(kArgsCreateFunction,
+ window_left, window_top,
+ size.width, size.height)),
+ keys::kInvalidWindowBoundsError))
+ << "size: " << size.width << "x" << size.height;
+ }
+
+ // Bounds with an empty size are not valid regardless of position.
+ EXPECT_TRUE(base::MatchPattern(
+ RunCreateWindowExpectError("[{\"width\": 0, \"height\": 0 }]"),
+ keys::kInvalidWindowBoundsError));
+
+ {
+ // A tiny window is valid as long as it is fully within a display.
+ const gfx::Point center =
+ display::Screen::Get()->GetPrimaryDisplay().bounds().CenterPoint();
+ auto function = base::MakeRefCounted<WindowsCreateFunction>();
+ function->set_extension(ExtensionBuilder("Test").Build().get());
+ EXPECT_TRUE(utils::RunFunction(
+ function.get(),
+ base::StringPrintf(kArgsCreateFunction, center.x(), center.y(), 1, 1),
+ profile(), api_test_utils::FunctionMode::kNone));
+ }
+}
+
IN_PROC_BROWSER_TEST_F(ExtensionWindowCreateTest, CreatePopupWindowFromWebUI) {
auto function = base::MakeRefCounted<WindowsCreateFunction>();
function->SetBrowserContextForTesting(profile());
Original Bug Report
Integer truncation in WindowBoundsIntersectDisplays allows off-screen windows from extensions without any permissions
VULNERABILITY DETAILS
It was discovered that Chrome extensions without any permissions can create and position browser windows entirely off-screen while still allowing them to receive user keyboard input.
To mitigate this class of issues, Chrome introduced the WindowBoundsIntersectDisplays function (https://chromium-review.googlesource.com/c/chromium/src/+/3536833
). This function was intended to prevent windows from being created or moved outside visible displays by computing the intersection area between the window bounds and all connected displays. The mitigation requires that at least 50% of the window area intersect with a display.
However, the comparison intersect_area >= (bounds.size().GetArea() / 2) relies on integer division, which introduces an issue. When the window area is 0 or 1, integer division truncates the result to 0, causing the check to always succeed, even if the window is completely off-screen.
As a result, an extension can:
- Create a window with normal, on-screen dimensions.
- Immediately resize the window so that its
widthorheightbecomes0, or both dimensions become1. - Move the window fully off-screen, bypassing the intersection check.
Integer division bypass
// chrome/browser/extensions/api/tabs/tabs_api.cc:421-437
bool WindowBoundsIntersectDisplays(const gfx::Rect& bounds) {
auto checked_area = bounds.size().GetCheckedArea();
if (!checked_area.IsValid()) {
return false;
}
int intersect_area = 0;
for (const auto& display : display::Screen::Get()->GetAllDisplays()) {
gfx::Rect display_bounds = display.bounds();
display_bounds.Intersect(bounds);
intersect_area += display_bounds.size().GetArea();
}
return intersect_area >= (bounds.size().GetArea() / 2);
}
When either the width or height is 0, or both width and height are 1:
- window_area = 0 * height = 0 (or 1 * 1 = 1).
- Integer division truncates: 0 / 2 = 0 (or 1 / 2 = 0)
- intersect_area = 0 when the window is fully off-screen.
- The check becomes: 0 >= 0, which always evaluates to true.
This allows the window to be considered valid regardless of its actual visibility.
Universal CSRF via element focus
The most impactful attack with minimal user interaction enabled by this issue in my opinion is a universal CSRF via element focus.
An extension can load a cross-origin page inside an off-screen window and automatically focus any element that has an id attribute by using a URL fragment (such as https://victim.com/oauth/authorize#btn-accept).
The browser will automatically focus the element with the specified id (id="btn-accept"). When the user presses Enter or Space, the focused element is activated, performing the action associated with the button.
This is particularly dangerous in the context of:
- OAuth authorization flows (a single keypress can silently grant access the a victim’s account).
- Payment confirmation dialogs (purchases can be completed without user awareness).
- Permission prompts (sensitive permissions may be granted unintentionally).
A page simulating an OAuth authorization flow is included to demonstrate this attack. I have also attached a video (repro.mp4) reproducing the issue. The video first shows the OAuth page working normally, and then demonstrates how the attack can trick the user into unknowingly approving the authorization without any visible UI or user awareness.
Additional impacts
Beyond the primary attack described above, prior research (see issue 40058935) also demonstrates that other sensitive browser features may be exploited using this technique. These impacts were not exhaustively validated, as they require additional user interaction.
| Impact | Keypresses |
|---|---|
| Screen share | 3 |
| Payment completion | 2+ |
| chrome://flags manipulation | 3 |
| chrome://settings changes | 3+ |
| External protocol launch | 2 |
| Pointer lock | 1 |
| Content leak via Ctrl+A and Ctrl+C | 2+ |
| Opening downloaded files | 2+ |
BISECT
An initial bisect identified the regression range between revisions 683284 and 683298 (https://chromium.googlesource.com/chromium/src/+log/ffaacd30fcc767d781cbef8c154d1f8a64a17ba2..b302b4c7894fa99cc33f8932469dca35fdf28c50).
Further investigation showed that the original behavior enabling off-screen windows was introduced by commit https://chromium.googlesource.com/chromium/src/+/df7de6e0612a769837271c884584633fa7375f6c, which landed in M78.0.3872.0.
A later attempt to mitigate a similar issue (https://chromium-review.googlesource.com/c/chromium/src/+/3536833) introduced the incomplete intersection logic, adding the integer truncation bug described above.
VERSION
Chrome Version: 144.0.7559.9
Chrome Version: 145.0.7632.5
Chrome Version: 146.0.7635.3
Chrome Version: 146.0.7646.0
Operating System: Windows 11 24H2
REPRODUCTION CASE
- Download
oauth.htmland place it in a directory. - Serve the file using a local web server at port
8080(e.g.,python -m http.server 8080). - Download
manifest.jsonandbackground.js. - Place those files in a separate directory and load the extension into Chrome.
- The extension creates a window and immediately resizes and moves it off-screen.
- The off-screen window loads
http://localhost:8080/oauth.html#btn-accept, which simulates an OAuth authorization page. - The URL includes the fragment
#btn-accept, automatically focusing theAcceptbutton. - Press
EnterorSpace, which triggers the button click without any visible UI, demonstrating the CSRF attack.
CREDIT INFORMATION
Reporter credit: Luan Herrera (@lbherrera_)
- http://localhost:8080/oauth.html#btn-accept
- https://chromium-review.googlesource.com/c/chromium/src/+/3536833
- https://chromium.googlesource.com/chromium/src/+/df7de6e0612a769837271c884584633fa7375f6c
- https://chromium.googlesource.com/chromium/src/+log/ffaacd30fcc767d781cbef8c154d1f8a64a17ba2..b302b4c7894fa99cc33f8932469dca35fdf28c50
- https://issuetracker.google.com/issues/40058935
- https://victim.com/oauth/authorize#btn-accept