Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient policy enforcement in Extensions
DescriptionInsufficient policy enforcement in Extensions
ComponentExtensions
Bug ClassLogic Error
Tracker513286820
Fix commit47d8324c988e (chromium/src) +120/-42
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
if
chrome/browser/extensions/api/tabs/tabs_api.cc
modified

Files Changed

  • chrome/browser/extensions/api/tabs/tabs_api.cc
  • chrome/browser/extensions/api/tabs/tabs_api.h
From 47d8324c988e265e193401a79554f5ac2a64213e Mon Sep 17 00:00:00 2001
From: pchodur <pchodur@google.com>
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 <finnur@chromium.org>
Commit-Queue: Patryk Chodur <pchodur@google.com>
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,
Loading diff…

Regression Test / PoC

shipped with the fix
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,
Loading diff…

Original Bug Report

reported by vm...@google.com

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.cc
  • chrome/browser/ui/navigator/browser_navigator.cc
  • chrome/browser/ui/tabs/tab_list_bridge.cc
  • chrome/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

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

  1. Install and launch an Isolated Web App (IWA).
  2. From a Chrome extension with the tabs permission, identify the tabId of the IWA and the windowId of a standard browser window.
  3. Call chrome.tabs.move(iwaTabId, {windowId: normalWindowId, index: -1}).
  4. Observe the IWA tab appearing in the standard window.
  5. 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.

View on issue tracker