Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in Isolated
DescriptionIncorrect authorization in Isolated
ComponentIsolated
Bug ClassLogic Error
Tracker517721914
Fix commit964c5f9e3bed (chromium/src) +106/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-09-08

Changed Functions

FunctionChangeNotes
Browser
chrome/browser/web_applications/isolated_web_apps/window_management/isolated_web_apps_window_open_permission_service_browsertest.cc
modified
IsolatedWebAppsWindowOpenPermissionServiceScopeExtensionBrowserTest
chrome/browser/web_applications/isolated_web_apps/window_management/isolated_web_apps_window_open_permission_service_browsertest.cc
modified

Files Changed

  • chrome/browser/ui/web_applications/navigation_capturing_process.cc
  • chrome/browser/web_applications/isolated_web_apps/window_management/BUILD.gn
  • chrome/browser/web_applications/isolated_web_apps/window_management/isolated_web_apps_window_open_permission_service_browsertest.cc
From 964c5f9e3beddaeb5c1accb7166dc6bc153e9819 Mon Sep 17 00:00:00 2001
From: Magdalena Skarbinska <[email protected]>
Date: Mon, 03 Aug 2026 03:25:17 -0700
Subject: [PATCH] [IWA] Fix IWA scope extension check in NavigationCapturingProcess

NavigationCapturingProcess::MaybeNotifyIwaTabCounterService previously
called FindBestAppWithUrlInScope for the initiator origin without
excluding scope extensions. As a result, when a window was opened from
an origin matching an IWA's scope extension, the navigation was
incorrectly attributed to the IWA as the opener.

This change passes {.exclude_scope_extensions = true} to
FindBestAppWithUrlInScope in NavigationCapturingProcess so that scope
extensions are excluded when resolving whether an IWA is the opener.
Adds a browser test NoNotificationForTabOpenedFromScopeExtensionOrigin
verifying no notification is emitted when opening a tab from a scope
extension origin.

Bug: 517721914
Change-Id: I9e3dd945d9452d0a76326e7e50b24785409185f6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8159009
Reviewed-by: Mike Wasserman <[email protected]>
Reviewed-by: Andrew Rayskiy <[email protected]>
Commit-Queue: Magdalena Skarbińska <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1672550}
---

diff --git a/chrome/browser/ui/web_applications/navigation_capturing_process.cc b/chrome/browser/ui/web_applications/navigation_capturing_process.cc
index a81948d..86341075 100644
--- a/chrome/browser/ui/web_applications/navigation_capturing_process.cc
+++ b/chrome/browser/ui/web_applications/navigation_capturing_process.cc
@@ -1057,7 +1057,8 @@
     }
 
     iwa_opener_app_id = provider->registrar_unsafe().FindBestAppWithUrlInScope(
-        initiator_origin->GetURL(), WebAppFilter::IsIsolatedApp());
+        initiator_origin->GetURL(), WebAppFilter::IsIsolatedApp(),
+        {.exclude_scope_extensions = true});
   }
 
   // If the "iwa_opener_app_id" is still not found, then there is a chance that
diff --git a/chrome/browser/web_applications/isolated_web_apps/window_management/BUILD.gn b/chrome/browser/web_applications/isolated_web_apps/window_management/BUILD.gn
index 09931fe0..b5841ccf 100644
--- a/chrome/browser/web_applications/isolated_web_apps/window_management/BUILD.gn
+++ b/chrome/browser/web_applications/isolated_web_apps/window_management/BUILD.gn
@@ -68,6 +68,7 @@
       "//components/permissions",
       "//components/webapps/common",
       "//components/webapps/isolated_web_apps/test_support",
+      "//components/webapps/services/web_app_origin_association/test:test_support",
       "//content/public/browser",
       "//content/test:test_support",
       "//ui/base",
diff --git a/chrome/browser/web_applications/isolated_web_apps/window_management/isolated_web_apps_window_open_permission_service_browsertest.cc b/chrome/browser/web_applications/isolated_web_apps/window_management/isolated_web_apps_window_open_permission_service_browsertest.cc
index 06e8e66..2006fbb2 100644
--- a/chrome/browser/web_applications/isolated_web_apps/window_management/isolated_web_apps_window_open_permission_service_browsertest.cc
+++ b/chrome/browser/web_applications/isolated_web_apps/window_management/isolated_web_apps_window_open_permission_service_browsertest.cc
@@ -9,6 +9,8 @@
 #include <vector>
 
 #include "base/i18n/message_formatter.h"
+#include "base/json/json_writer.h"
+#include "base/strings/stringprintf.h"
 #include "base/strings/utf_string_conversions.h"
 #include "base/test/simple_test_clock.h"
 #include "base/test/test_future.h"
@@ -26,16 +28,24 @@
 #include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
 #include "chrome/browser/web_applications/test/web_app_test_utils.h"
 #include "chrome/browser/web_applications/web_app_command_scheduler.h"
+#include "chrome/browser/web_applications/web_app_origin_association_manager.h"
 #include "chrome/browser/web_applications/web_app_provider.h"
 #include "chrome/browser/web_applications/web_app_registrar.h"
 #include "chrome/test/base/in_process_browser_test.h"
+#include "chrome/test/base/ui_test_utils.h"
 #include "components/webapps/common/web_app_id.h"
 #include "components/webapps/isolated_web_apps/test_support/signing_keys.h"
+#include "components/webapps/services/web_app_origin_association/test/test_web_app_origin_association_fetcher.h"
+#include "content/public/browser/page_navigator.h"
+#include "content/public/browser/render_frame_host.h"
+#include "content/public/browser/site_instance.h"
 #include "content/public/browser/web_contents.h"
 #include "content/public/test/browser_test.h"
 #include "content/public/test/browser_test_utils.h"
 #include "content/public/test/test_navigation_observer.h"
+#include "net/test/embedded_test_server/embedded_test_server.h"
 #include "ui/base/l10n/l10n_util.h"
+#include "url/origin.h"
 
 class Browser;
 
@@ -502,4 +512,97 @@
                                 GURL("https://example.com/child1"));
   EXPECT_EQ(0u, GetNotificationCount());
 }
+
+class IsolatedWebAppsWindowOpenPermissionServiceScopeExtensionBrowserTest
+    : public IsolatedWebAppsWindowOpenPermissionServiceBrowserTest {
+ public:
+  void SetUpOnMainThread() override {
+    IsolatedWebAppsWindowOpenPermissionServiceBrowserTest::SetUpOnMainThread();
+
+    auto fetcher =
+        std::make_unique<webapps::TestWebAppOriginAssociationFetcher>();
+    origin_association_fetcher_ = fetcher.get();
+    provider().origin_association_manager().SetFetcherForTest(
+        std::move(fetcher));
+  }
+
+  void TearDownOnMainThread() override {
+    origin_association_fetcher_ = nullptr;
+    IsolatedWebAppsWindowOpenPermissionServiceBrowserTest::
+        TearDownOnMainThread();
+  }
+
+ protected:
+  webapps::AppId InstallIsolatedWebAppWithScopeExtension(
+      const url::Origin& scope_extension_origin) {
+    origin_association_fetcher_->SetData(
+        {{scope_extension_origin,
+          *base::WriteJson(base::DictValue().Set(
+              base::StringPrintf(
+                  "isolated-app://%s/",
+                  web_package::test::GetDefaultEd25519WebBundleId()
+                      .id()
+                      .c_str()),
+              base::DictValue().Set("scope", "/")))}});
+
+    webapps::AppId app_id =
+        IsolatedWebAppBuilder(
+            ManifestBuilder()
+                .SetName(kIsolatedApp1DefaultName)
+                .SetVersion(kIsolatedAppVersion)
+                .AddScopeExtension(scope_extension_origin,
+                                   /*has_origin_wildcard=*/false))
+            .BuildBundle(web_package::test::GetDefaultEd25519KeyPair())
+            ->InstallChecked(profile())
+            .app_id();
+    EXPECT_EQ(1u, provider()
+                      .registrar_unsafe()
+                      .GetValidatedScopeExtensions(app_id)
+                      .size());
+    return app_id;
+  }
+
+ private:
+  raw_ptr<webapps::TestWebAppOriginAssociationFetcher>
+      origin_association_fetcher_ = nullptr;
+};
+
+IN_PROC_BROWSER_TEST_F(
+    IsolatedWebAppsWindowOpenPermissionServiceScopeExtensionBrowserTest,
+    NoNotificationForTabOpenedFromScopeExtensionOrigin) {
+  url::Origin scope_extension_origin = embedded_https_test_server().GetOrigin();
+  webapps::AppId app_id =
+      InstallIsolatedWebAppWithScopeExtension(scope_extension_origin);
+
+  // Open the scope-extension origin in a regular browser tab (not in the IWA).
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(
+      browser(), embedded_https_test_server().GetURL("/simple.html")));
+  content::WebContents* opener_contents =
+      browser()->tab_strip_model()->GetActiveWebContents();
+  ASSERT_EQ(scope_extension_origin,
+            opener_contents->GetPrimaryMainFrame()->GetLastCommittedOrigin());
+
+  // Open a new background tab from the scope-extension page, as the renderer
+  // does for e.g. a middle-click on a link.
+  content::OpenURLParams params(
+      GURL("https://example.com/destination"), content::Referrer(),
+      WindowOpenDisposition::NEW_BACKGROUND_TAB, ui::PAGE_TRANSITION_LINK,
+      /*is_renderer_initiated=*/true);
+  params.initiator_origin = scope_extension_origin;
+  params.source_site_instance =
+      opener_contents->GetPrimaryMainFrame()->GetSiteInstance();
+  params.user_gesture = true;
+
+  content::WebContentsAddedObserver new_contents_observer;
+  opener_contents->OpenURL(params, /*navigation_handle_callback=*/{});
+  content::WebContents* new_contents = new_contents_observer.GetWebContents();
+  WaitForLoadStopWithoutSuccessCheck(new_contents);
+
+  // The IWA itself never opened a window, so it must not be attributed as the
+  // opener.
+  EXPECT_EQ(0u, GetNotificationCount());
+  EXPECT_FALSE(
+      display_service_tester_->GetNotification(GetNotificationIdForApp(app_id))
+          .has_value());
+}
 }  // namespace web_app
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/web_applications/isolated_web_apps/window_management/isolated_web_apps_window_open_permission_service_browsertest.cc b/chrome/browser/web_applications/isolated_web_apps/window_management/isolated_web_apps_window_open_permission_service_browsertest.cc
index 06e8e66..2006fbb2 100644
--- a/chrome/browser/web_applications/isolated_web_apps/window_management/isolated_web_apps_window_open_permission_service_browsertest.cc
+++ b/chrome/browser/web_applications/isolated_web_apps/window_management/isolated_web_apps_window_open_permission_service_browsertest.cc
@@ -9,6 +9,8 @@
 #include <vector>
 
 #include "base/i18n/message_formatter.h"
+#include "base/json/json_writer.h"
+#include "base/strings/stringprintf.h"
 #include "base/strings/utf_string_conversions.h"
 #include "base/test/simple_test_clock.h"
 #include "base/test/test_future.h"
@@ -26,16 +28,24 @@
 #include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
 #include "chrome/browser/web_applications/test/web_app_test_utils.h"
 #include "chrome/browser/web_applications/web_app_command_scheduler.h"
+#include "chrome/browser/web_applications/web_app_origin_association_manager.h"
 #include "chrome/browser/web_applications/web_app_provider.h"
 #include "chrome/browser/web_applications/web_app_registrar.h"
 #include "chrome/test/base/in_process_browser_test.h"
+#include "chrome/test/base/ui_test_utils.h"
 #include "components/webapps/common/web_app_id.h"
 #include "components/webapps/isolated_web_apps/test_support/signing_keys.h"
+#include "components/webapps/services/web_app_origin_association/test/test_web_app_origin_association_fetcher.h"
+#include "content/public/browser/page_navigator.h"
+#include "content/public/browser/render_frame_host.h"
+#include "content/public/browser/site_instance.h"
 #include "content/public/browser/web_contents.h"
 #include "content/public/test/browser_test.h"
 #include "content/public/test/browser_test_utils.h"
 #include "content/public/test/test_navigation_observer.h"
+#include "net/test/embedded_test_server/embedded_test_server.h"
 #include "ui/base/l10n/l10n_util.h"
+#include "url/origin.h"
 
 class Browser;
 
@@ -502,4 +512,97 @@
                                 GURL("https://example.com/child1"));
   EXPECT_EQ(0u, GetNotificationCount());
 }
+
+class IsolatedWebAppsWindowOpenPermissionServiceScopeExtensionBrowserTest
+    : public IsolatedWebAppsWindowOpenPermissionServiceBrowserTest {
+ public:
+  void SetUpOnMainThread() override {
+    IsolatedWebAppsWindowOpenPermissionServiceBrowserTest::SetUpOnMainThread();
+
+    auto fetcher =
+        std::make_unique<webapps::TestWebAppOriginAssociationFetcher>();
+    origin_association_fetcher_ = fetcher.get();
+    provider().origin_association_manager().SetFetcherForTest(
+        std::move(fetcher));
+  }
+
+  void TearDownOnMainThread() override {
+    origin_association_fetcher_ = nullptr;
+    IsolatedWebAppsWindowOpenPermissionServiceBrowserTest::
+        TearDownOnMainThread();
+  }
+
+ protected:
+  webapps::AppId InstallIsolatedWebAppWithScopeExtension(
+      const url::Origin& scope_extension_origin) {
+    origin_association_fetcher_->SetData(
+        {{scope_extension_origin,
+          *base::WriteJson(base::DictValue().Set(
+              base::StringPrintf(
+                  "isolated-app://%s/",
+                  web_package::test::GetDefaultEd25519WebBundleId()
+                      .id()
+                      .c_str()),
+              base::DictValue().Set("scope", "/")))}});
+
+    webapps::AppId app_id =
+        IsolatedWebAppBuilder(
+            ManifestBuilder()
+                .SetName(kIsolatedApp1DefaultName)
+                .SetVersion(kIsolatedAppVersion)
+                .AddScopeExtension(scope_extension_origin,
+                                   /*has_origin_wildcard=*/false))
+            .BuildBundle(web_package::test::GetDefaultEd25519KeyPair())
+            ->InstallChecked(profile())
+            .app_id();
+    EXPECT_EQ(1u, provider()
+                      .registrar_unsafe()
+                      .GetValidatedScopeExtensions(app_id)
+                      .size());
+    return app_id;
+  }
+
+ private:
+  raw_ptr<webapps::TestWebAppOriginAssociationFetcher>
+      origin_association_fetcher_ = nullptr;
+};
+
+IN_PROC_BROWSER_TEST_F(
+    IsolatedWebAppsWindowOpenPermissionServiceScopeExtensionBrowserTest,
+    NoNotificationForTabOpenedFromScopeExtensionOrigin) {
+  url::Origin scope_extension_origin = embedded_https_test_server().GetOrigin();
+  webapps::AppId app_id =
+      InstallIsolatedWebAppWithScopeExtension(scope_extension_origin);
+
+  // Open the scope-extension origin in a regular browser tab (not in the IWA).
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(
+      browser(), embedded_https_test_server().GetURL("/simple.html")));
+  content::WebContents* opener_contents =
+      browser()->tab_strip_model()->GetActiveWebContents();
+  ASSERT_EQ(scope_extension_origin,
+            opener_contents->GetPrimaryMainFrame()->GetLastCommittedOrigin());
+
+  // Open a new background tab from the scope-extension page, as the renderer
+  // does for e.g. a middle-click on a link.
+  content::OpenURLParams params(
+      GURL("https://example.com/destination"), content::Referrer(),
+      WindowOpenDisposition::NEW_BACKGROUND_TAB, ui::PAGE_TRANSITION_LINK,
+      /*is_renderer_initiated=*/true);
+  params.initiator_origin = scope_extension_origin;
+  params.source_site_instance =
+      opener_contents->GetPrimaryMainFrame()->GetSiteInstance();
+  params.user_gesture = true;
+
+  content::WebContentsAddedObserver new_contents_observer;
+  opener_contents->OpenURL(params, /*navigation_handle_callback=*/{});
+  content::WebContents* new_contents = new_contents_observer.GetWebContents();
+  WaitForLoadStopWithoutSuccessCheck(new_contents);
+
+  // The IWA itself never opened a window, so it must not be attributed as the
+  // opener.
+  EXPECT_EQ(0u, GetNotificationCount());
+  EXPECT_FALSE(
+      display_service_tester_->GetNotification(GetNotificationIdForApp(app_id))
+          .has_value());
+}
 }  // namespace web_app
Loading diff…

Original Bug Report

reported by [email protected]

Potential IWA warning quota exhaustion via initiator fallback scope extension mismatch

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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: In NavigationCapturingProcess::MaybeNotifyIwaTabCounterService, resolving an IWA opener via the initiator-origin fallback fails to exclude scope extensions. As a result, an ordinary HTTPS origin listed in an IWA’s validated scope extensions can falsely match as the IWA itself. This allows a regular website to trigger misleading system notifications and exhaust the safety warning quota for the associated IWA.

Affected files:

  • chrome/browser/ui/web_applications/navigation_capturing_process.cc
  • chrome/browser/web_applications/isolated_web_apps/window_management/isolated_web_apps_window_open_permission_service.cc

Estimated timestamp from git blame: 2025-10-30

Potential Security Mitigation Bypass and UI Spoofing in IWA Tab Counter Service

Root Cause Analysis

In NavigationCapturingProcess::MaybeNotifyIwaTabCounterService (chrome/browser/ui/web_applications/navigation_capturing_process.cc), the potential IWA opener is resolved via an initiator-origin fallback when an opener-chain app ID is not present:

// chrome/browser/ui/web_applications/navigation_capturing_process.cc
if (!iwa_opener_app_id) {
  if (!navigation_handle) {
    return;
  }
  const auto& initiator_origin = navigation_handle->GetInitiatorOrigin();
  if (!initiator_origin) {
    return;
  }
  iwa_opener_app_id = provider->registrar_unsafe().FindBestAppWithUrlInScope(
      initiator_origin->GetURL(), WebAppFilter::IsIsolatedApp());
}

However, the call to FindBestAppWithUrlInScope uses the 2-argument overload (defined in chrome/browser/web_applications/web_app_registrar.h), which defaults scope_score_options to {}. Consequently, exclude_scope_extensions is false during the scope resolution.

This causes GetScopeScore to walk the target app’s validated_scope_extensions. If a regular HTTPS origin (e.g., https://ext.example) is listed in a user-installed Isolated Web App’s (IWA) validated_scope_extensions, the IWA wins the scope match. Since the post-match check only tests the resolved app’s isolation properties rather than the initiator’s scheme, the initiator is incorrectly treated as the IWA.

This is an anti-pattern that has previously been fixed at other security-sensitive sites, such as:

  1. DeviceServiceImpl::IsTrustedContext (chrome/browser/device_api/device_service_impl.cc:112)
  2. WebAppTabHelper::FindAppIdForUrl (chrome/browser/web_applications/web_app_tab_helper.cc:60)

In both cases, {.exclude_scope_extensions = true} is explicitly passed to ensure regular HTTPS origins in scope extensions cannot pretend to be the trusted IWA.

Potential Trigger Path

Note: These are potential steps for a proof of concept; our tooling agent does not have the ability to run code.

  1. A user-installed IWA (matching IsIsolatedWebAppWithOnlyUserManagement) declares https://ext.example in its scope_extensions, and the origin has opted in via .well-known/web-app-origin-association (successfully registered in validated_scope_extensions).
  2. The user has https://ext.example open in an ordinary browser tab and clicks an <a target="_blank" rel="noopener"> link or executes window.open without keeping an opener reference.
  3. This is routed to MaybeNotifyIwaTabCounterService with the initiator origin set to https://ext.example but no live opener chain.
  4. The initiator-origin fallback invokes FindBestAppWithUrlInScope without excluding scope extensions, potentially resolving iwa_opener_app_id to the IWA.
  5. IsolatedWebAppsWindowOpenPermissionService::OnWebContentsCreated is notified with this App ID.

Potential Impact

An HTTPS origin listed in an IWA’s scope_extensions can potentially cause the following security issues:

  1. False Attribution UI Notification: It triggers a trusted ChromeOS system notification ("<IWA name> is opening tabs") with a settings button pointing to the IWA’s popup-permission page. This presents a trusted UI misattribution/spoofing vector.
  2. Safety Warning Quota Exhaustion: It silently increments and persists the IWA’s opened_tabs_counter_notification_state().times_shown up to kMaxNotificationShowCount = 3 in the WebApp database. Once this safety quota is exhausted, the user is never again warned when the IWA itself actually spawns many tabs or windows in the future, effectively bypassing a built-in safety boundary.

Suggested Fix

Update the fallback logic in NavigationCapturingProcess::MaybeNotifyIwaTabCounterService to explicitly exclude scope extensions during resolution:

iwa_opener_app_id = provider->registrar_unsafe().FindBestAppWithUrlInScope(
    initiator_origin->GetURL(), WebAppFilter::IsIsolatedApp(),
    {.exclude_scope_extensions = true});

Evaluated with Chrome root at commit: 5133b93d189b383c37805b1cf3a9d2dbfe8d7379


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