CVE-2026-13948
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/extensions/api/tabs/tabs_api.cc |
modified |
Files Changed
chrome/browser/extensions/api/tabs/tabs_api.ccchrome/browser/extensions/api/tabs/tabs_api.h
Patch
From 47d8324c988e265e193401a79554f5ac2a64213e Mon Sep 17 00:00:00 2001 From: pchodur <[email protected]> Date: Mon, 18 May 2026 13:18:41 -0700 Subject: [PATCH] Block moving IWA to a tabbed browser by extensions This commit blocks ability to move the IWA to a tabbed browser by extensions using chrome.tabs.group and chrome.tabs.move. The check for IWA was left out in the MoveTabToWindow, but present in the ValidateTab method. Now the checks are centralized within the ValidateTab method. Bug: 513286820 Link: https://chromium-review.googlesource.com/id/Id8201f30c51d3f0435ec04fe09c7ef356a6a6964 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7852566 Reviewed-by: Finnur Thorarinsson <[email protected]> Commit-Queue: Patryk Chodur <[email protected]> Cr-Commit-Position: refs/heads/main@{#1632365} --- diff --git a/chrome/browser/extensions/api/tabs/tabs_api.cc b/chrome/browser/extensions/api/tabs/tabs_api.cc index 0801bd6..d8e00eb 100644 --- a/chrome/browser/extensions/api/tabs/tabs_api.cc +++ b/chrome/browser/extensions/api/tabs/tabs_api.cc @@ -11,6 +11,7 @@ #include "base/strings/utf_string_conversions.h" #include "base/task/bind_post_task.h" #include "base/task/thread_pool.h" +#include "base/types/expected.h" #include "base/types/expected_macros.h" #include "base/types/optional_util.h" #include "chrome/browser/devtools/devtools_window.h" @@ -131,8 +132,8 @@ constexpr char kWindowCreateCannotUseTabIdWithIwaError[] = "Creating a new window for an Isolated Web App does not support adding a " "tab by its ID."; -constexpr char kWindowCreateCannotMoveIwaTabError[] = - "The tab of an Isolated Web App cannot be moved to a new window."; +constexpr char kCannotMoveIwaTabError[] = + "The tab of an Isolated Web App cannot be moved."; #endif #if BUILDFLAG(IS_ANDROID) @@ -403,17 +404,20 @@ bool allow_other_window_types, std::string* error) { WindowController* source_window = nullptr; + content::WebContents* web_contents = nullptr; int source_index = -1; if (!tabs_internal::GetTabById(tab_id, function->browser_context(), function->include_incognito_information(), - &source_window, nullptr, &source_index, + &source_window, &web_contents, &source_index, error) || !source_window) { return -1; } - if (!ExtensionTabUtil::IsTabStripEditable(*source_window->profile())) { - *error = ExtensionTabUtil::kTabStripNotEditableError; + auto validation_result = WindowsCreateFunction::ValidateTab( + source_window, target_browser->GetProfile(), web_contents); + if (!validation_result.has_value()) { + *error = std::move(validation_result.error()); return -1; } @@ -426,11 +430,6 @@ return -1; } - if (target_browser->GetProfile() != source_window->profile()) { - *error = ExtensionTabUtil::kCanOnlyMoveTabsWithinSameProfileError; - return -1; - } - TabListInterface* target_tab_list = ExtensionTabUtil::GetEditableTabList(*target_browser); CHECK(target_tab_list); @@ -463,10 +462,7 @@ BrowserWindowInterface* source_browser = source_window->GetBrowserWindowInterface(); - if (!source_browser) { - *error = ExtensionTabUtil::kCanOnlyMoveTabsWithinNormalWindowsError; - return -1; - } + CHECK(source_browser); TabListInterface* source_tab_list = TabListInterface::From(source_browser); ::tabs::TabInterface* tab = source_tab_list->GetTab(source_index); @@ -997,10 +993,10 @@ } // Validate the tab information. Return an error if it's not valid. - std::string tab_error = ValidateTab(source_window, window_profile, - web_contents, is_locked_fullscreen); - if (!tab_error.empty()) { - return RespondNow(Error(std::move(tab_error))); + auto tab_validation = ValidateTab(source_window, window_profile, + web_contents, is_locked_fullscreen); + if (!tab_validation.has_value()) { + return RespondNow(Error(std::move(tab_validation.error()))); } } @@ -1385,39 +1381,40 @@ } // static -std::string WindowsCreateFunction::ValidateTab( +base::expected<void, std::string> WindowsCreateFunction::ValidateTab( WindowController* source_window, Profile* window_profile, content::WebContents* web_contents, bool is_locked_fullscreen) { if (!source_window) { // The source window can be null for prerender tabs. - return tabs_constants::kInvalidWindowStateError; + return base::unexpected(tabs_constants::kInvalidWindowStateError); } - if (!source_window->GetBrowserWindowInterface()) { - return ExtensionTabUtil::kCanOnlyMoveTabsWithinNormalWindowsError; + return base::unexpected( + ExtensionTabUtil::kCanOnlyMoveTabsWithinNormalWindowsError); } - #if !BUILDFLAG(IS_ANDROID) Browser* source_browser = source_window->GetBrowser(); + CHECK(source_browser); if (web_app::AppBrowserController* controller = source_browser->app_controller(); controller && controller->IsIsolatedWebApp()) { - return kWindowCreateCannotMoveIwaTabError; + return base::unexpected(kCannotMoveIwaTabError); } #endif - if (!ExtensionTabUtil::IsTabStripEditable(*window_profile)) { - return ExtensionTabUtil::kTabStripNotEditableError; + if (!ExtensionTabUtil::IsTabStripEditable(*source_window->profile())) { + return base::unexpected(ExtensionTabUtil::kTabStripNotEditableError); } if (source_window->profile() != window_profile) { - return ExtensionTabUtil::kCanOnlyMoveTabsWithinSameProfileError; + return base::unexpected( + ExtensionTabUtil::kCanOnlyMoveTabsWithinSameProfileError); } if (DevToolsWindow::IsDevToolsWindow(web_contents)) { - return tabs_constants::kNotAllowedForDevToolsError; + return base::unexpected(tabs_constants::kNotAllowedForDevToolsError); } #if BUILDFLAG(IS_CHROMEOS) @@ -1425,11 +1422,12 @@ // locked fullscreen on ChromeOS. if (is_locked_fullscreen && ash::features::IsBocaOnTaskLockedQuizMigrationEnabled()) { - return ExtensionTabUtil::kCanOnlyMoveTabsWithinNormalWindowsError; + return base::unexpected( + ExtensionTabUtil::kCanOnlyMoveTabsWithinNormalWindowsError); } #endif // BUILDFLAG(IS_CHROMEOS) - return std::string(); // No error. + return {}; } // static diff --git a/chrome/browser/extensions/api/tabs/tabs_api.h b/chrome/browser/extensions/api/tabs/tabs_api.h index 0ac0d29..065f696 100644 --- a/chrome/browser/extensions/api/tabs/tabs_api.h +++ b/chrome/browser/extensions/api/tabs/tabs_api.h @@ -12,6 +12,7 @@ #include "base/memory/raw_ptr.h" #include "base/memory/raw_ref.h" #include "base/memory/scoped_refptr.h" +#include "base/types/expected.h" #include "base/values.h" #include "chrome/browser/extensions/chrome_extension_function_details.h" #include "chrome/browser/extensions/window_controller.h" @@ -191,16 +192,16 @@ ResponseAction Run() override; DECLARE_EXTENSION_FUNCTION("windows.create", WINDOWS_CREATE) + // Ensures the tab for the window is valid. + static base::expected<void, std::string> ValidateTab( + WindowController* source_window, + Profile* window_profile, + content::WebContents* web_contents, + bool is_locked_fullscreen = false); + private: ~WindowsCreateFunction() override; - // Ensures the tab for the window is valid. Returns an error string, or the - // empty string if the tab is valid. - static std::string ValidateTab(WindowController* source_window, - Profile* window_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 b67ec4b..448630f6 100644
--- a/chrome/browser/extensions/api/tabs/tabs_test.cc
+++ b/chrome/browser/extensions/api/tabs/tabs_test.cc
@@ -1397,12 +1397,9 @@
std::string args;
};
-// Test that `windows.create` functions correctly for Isolated Web Apps.
-class ExtensionWindowCreateIwaTest
- : public InProcessBrowserTest,
- public testing::WithParamInterface<ExtensionWindowCreateIwaParam> {
+class ExtensionIwaTestBase : public InProcessBrowserTest {
public:
- ExtensionWindowCreateIwaTest() {
+ ExtensionIwaTestBase() {
scoped_feature_list_.InitAndEnableFeature(features::kIsolatedWebApps);
set_open_about_blank_on_browser_launch(false);
}
@@ -1452,6 +1449,14 @@
base::ScopedTempDir scoped_temp_dir_;
};
+// Test that `windows.create` functions correctly for Isolated Web Apps.
+class ExtensionWindowCreateIwaTest
+ : public ExtensionIwaTestBase,
+ public testing::WithParamInterface<ExtensionWindowCreateIwaParam> {
+ public:
+ ExtensionWindowCreateIwaTest() = default;
+};
+
IN_PROC_BROWSER_TEST_P(ExtensionWindowCreateIwaTest, CreateWindowForIwa) {
auto url_info = InstallAndTrustBundle();
@@ -1560,6 +1565,80 @@
[](const testing::TestParamInfo<ExtensionWindowCreateIwaTest::ParamType>&
info) { return info.param.test_name; });
+class ExtensionApiTabsIwaMoveTest : public ExtensionIwaTestBase {
+ public:
+ ExtensionApiTabsIwaMoveTest() = default;
+
+ protected:
+ BrowserWindowInterface* OpenIwa(
+ const web_app::IsolatedWebAppUrlInfo& url_info) {
+ scoped_refptr<const Extension> extension =
+ ExtensionBuilder("IwaOpenerExtension").Build();
+ auto function = base::MakeRefCounted<WindowsCreateFunction>();
+ function->set_extension(extension);
+
+ std::string args = base::StringPrintf(
+ R"([{"url": "%s"}])", url_info.origin().GetURL().spec().c_str());
+
+ bool result = api_test_utils::RunFunction(
+ function.get(), args, profile(), api_test_utils::FunctionMode::kNone);
+ EXPECT_TRUE(result) << function->GetError();
+
+ BrowserWindowInterface* iwa_browser =
+ GetLastActiveBrowserWindowInterfaceWithAnyProfile();
+ EXPECT_TRUE(iwa_browser);
+ return iwa_browser;
+ }
+};
+
+IN_PROC_BROWSER_TEST_F(ExtensionApiTabsIwaMoveTest, CannotMoveIwaTab) {
+ auto url_info = InstallAndTrustBundle();
+ BrowserWindowInterface* iwa_browser = OpenIwa(url_info);
+
+ TabListInterface* iwa_tab_list = TabListInterface::From(iwa_browser);
+ ASSERT_EQ(iwa_tab_list->GetTabCount(), 1);
+ int iwa_tab_id =
+ ExtensionTabUtil::GetTabId(iwa_tab_list->GetTab(0)->GetContents());
+
+ Browser* normal_browser = CreateBrowser(profile());
+ int target_window_id = ExtensionTabUtil::GetWindowId(normal_browser);
+
+ auto function = base::MakeRefCounted<TabsMoveFunction>();
+
+ std::string args = base::StringPrintf(
+ R"([%d, {"windowId": %d, "index": -1}])", iwa_tab_id, target_window_id);
+
+ std::string error = api_test_utils::RunFunctionAndReturnError(
+ function.get(), args, profile());
+
+ EXPECT_EQ(error, "The tab of an Isolated Web App cannot be moved.");
+}
+
+IN_PROC_BROWSER_TEST_F(ExtensionApiTabsIwaMoveTest,
+ CannotGroupIwaTabToOtherWindow) {
+ auto url_info = InstallAndTrustBundle();
+ BrowserWindowInterface* iwa_browser = OpenIwa(url_info);
+
+ TabListInterface* iwa_tab_list = TabListInterface::From(iwa_browser);
+ ASSERT_EQ(iwa_tab_list->GetTabCount(), 1);
+ int iwa_tab_id =
+ ExtensionTabUtil::GetTabId(iwa_tab_list->GetTab(0)->GetContents());
+
+ Browser* normal_browser = CreateBrowser(profile());
+ int target_window_id = ExtensionTabUtil::GetWindowId(normal_browser);
+
+ auto function = base::MakeRefCounted<TabsGroupFunction>();
+
+ std::string args = base::StringPrintf(
+ R"([{"tabIds": [%d], "createProperties": {"windowId": %d}}])", iwa_tab_id,
+ target_window_id);
+
+ std::string error = api_test_utils::RunFunctionAndReturnError(
+ function.get(), args, profile());
+
+ EXPECT_EQ(error, "The tab of an Isolated Web App cannot be moved.");
+}
+
IN_PROC_BROWSER_TEST_F(ExtensionTabsTest, DuplicateTab) {
content::OpenURLParams params(GURL(url::kAboutBlankURL), content::Referrer(),
WindowOpenDisposition::NEW_FOREGROUND_TAB,
Original Bug Report
Bypass of Isolated Web App window isolation via chrome.tabs extension APIs
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A logic error in the Chrome Extensions Tabs API allows an extension to move an Isolated Web App (IWA) tab into a standard browser window. This bypasses the intended window isolation for IWAs and leads to a browser process crash when the tab is navigated.
Affected files:
chrome/browser/extensions/api/tabs/tabs_api.ccchrome/browser/ui/navigator/browser_navigator.ccchrome/browser/ui/tabs/tab_list_bridge.ccchrome/browser/extensions/extension_tab_util.cc
Estimated timestamp from git blame: 2024-02-12
Summary
Isolated Web Apps (IWAs) are required to run in standalone application windows (TYPE_APP) to maintain a trusted UI context and separate them from regular web content. A potential vulnerability in the chrome.tabs extension API allows an extension to move an IWA tab into a standard (TYPE_NORMAL) browser window. This violates IWA security invariants and results in a browser crash upon navigation due to a security CHECK failure.
Root Cause Analysis
In chrome/browser/extensions/api/tabs/tabs_api.cc, the helper function MoveTabToWindow is used by several APIs, including chrome.tabs.move and chrome.tabs.group. Unlike the validation logic used when creating new windows (WindowsCreateFunction::ValidateTab), MoveTabToWindow fails to check if the tab being moved belongs to an Isolated Web App.
When an extension dispatches a move request for an IWA tab, the WebContents is reparented from its dedicated app window to a standard browser window. This leaves the IWA in a window that lacks an AppBrowserController. Consequently, when the IWA tab is refreshed or navigated, the high-level navigation logic in chrome/browser/ui/navigator/browser_navigator.cc dispatches a CHECK to ensure the hosting window is a valid IWA window:
// chrome/browser/ui/navigator/browser_navigator.cc:715
if (content::SiteIsolationPolicy::ShouldUrlUseApplicationIsolationLevel(
params->initiating_profile, params->url)) {
CHECK(web_app::AppBrowserController::IsIsolatedWebApp(params->browser));
}
Because the tab is now in a TYPE_NORMAL window, IsIsolatedWebApp returns false, triggering the CHECK and crashing the browser process.
Impact
- Security Boundary Violation: The bypass allows IWA content to be mixed with untrusted web content in the same window, which IWAs are specifically designed to avoid to prevent UI spoofing and phishing.
- Denial of Service: An extension can reliably crash the entire browser process by moving and then refreshing an IWA tab.
Potential Reproduction Steps
Note: These steps are based on source code analysis; the ability to execute code was not available during this review.
- Install and launch an Isolated Web App (IWA).
- From a Chrome extension with the
tabspermission, identify thetabIdof the IWA and thewindowIdof a standard browser window. - Call
chrome.tabs.move(iwaTabId, {windowId: normalWindowId, index: -1}). - Observe the IWA tab appearing in the standard window.
- Refresh the IWA tab to trigger a browser process crash.
Suggested Fix
The MoveTabToWindow helper in chrome/browser/extensions/api/tabs/tabs_api.cc should be updated to validate the source tab. If the tab is an Isolated Web App, the move request should be rejected with an error, ensuring IWAs remain within their authorized application containers.
Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.