Medium watch CVSS 5.4 chrome Authorization Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
5.4
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in Isolated
DescriptionIncorrect authorization in Isolated
ComponentIsolated
Sub-componentIWA
Bug ClassAuthorization Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker517721914
Fix commit964c5f9e3bed (chromium/src) +106/-1
CWECWE-863
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:L
CISA KEVNot listed
CreditedGoogle
Reported2026-05-29 (per vendor advisory)
Fix landed2026-08-03 (public git)
Disclosed2026-09-08
Patch gap36 days — fix public in source before the advisory

Background

Isolated Web App (IWA)
a packaged web application served from an isolated-app:// origin whose pages run in a dedicated, isolated context with elevated capabilities.
Scope extension
an additional external origin that an IWA declares (via AddScopeExtension / a validated origin association) so that origin’s pages are treated as belonging to the app’s scope, distinct from the app’s own primary origin.
`FindBestAppWithUrlInScope`
a WebAppRegistrar lookup that, given a URL and a WebAppFilter, returns the app whose scope best matches the URL; it can optionally count or exclude scope extensions when matching.
`NavigationCapturingProcess`
the browser-side logic that decides how a navigation/window-open is attributed to a web app, including whether an IWA should be treated as the opener of a new tab.

Root Cause Analysis

The changed call site in chrome/browser/ui/web_applications/navigation_capturing_process.cc computed iwa_opener_app_id by calling FindBestAppWithUrlInScope(initiator_origin->GetURL(), WebAppFilter::IsIsolatedApp()) without any option controlling scope-extension matching. Because the default match includes scope extensions, an initiator_origin that merely matched an IWA’s declared scope extension (not the IWA’s own isolated-app:// origin) resolved to that IWA, so a window opened from an ordinary web origin was incorrectly attributed to the IWA as the opener. This violates the invariant that IWA-as-opener attribution should reflect only the IWA itself acting, not any external origin that happens to be inside the app’s extended scope.

The fix passes {.exclude_scope_extensions = true} to the same lookup, so extension origins no longer resolve to the IWA and the attribution is only made when the initiator truly belongs to the app’s own scope. The added browser test NoNotificationForTabOpenedFromScopeExtensionOrigin confirms that opening a tab from a scope-extension origin now emits no IWA notification.

Key insight
The single mistake is calling FindBestAppWithUrlInScope with scope-extension matching left enabled when resolving the IWA opener, which let a non-IWA scope-extension origin be authorized as the IWA opener; the fix addresses it by explicitly excluding scope extensions ({.exclude_scope_extensions = true}) from that resolution.

Attack Path

  1. Set up an IWA with a scope extension An IWA is installed that declares a scope extension for an attacker-reachable external origin, so that origin is validated as part of the app’s extended scope.
  2. Navigate the scope-extension origin outside the IWA A user (or the attacker’s page) loads the scope-extension origin in a regular browser tab, not inside the IWA itself, as the test does via NavigateToURL to that origin.
  3. Trigger a window-open from that origin The scope-extension page initiates a new tab/window (the test uses a renderer-initiated OpenURL with initiator_origin set to the scope-extension origin), driving NavigationCapturingProcess to resolve the IWA opener.
  4. Incorrect opener attribution FindBestAppWithUrlInScope matches the initiator against the IWA’s scope extension and returns the IWA’s iwa_opener_app_id, so the navigation is authorized/attributed as though the IWA opened it even though the IWA never did — evidenced by the pre-fix code emitting the IWA notification the test now asserts must be absent.
  5. Downstream escalation Any capability or trust granted on the basis of being an IWA opener would then be exposed to the external origin (inferred); the diff itself only demonstrates the erroneous attribution/notification, not a further primitive.

Impact Assessment

An attacker controlling (or able to serve pages from) an origin that is a validated scope extension of an installed IWA could cause a window-open to be incorrectly authorized/attributed to the IWA as opener in the browser process, without the IWA itself acting. The advisory classifies this as an AuthorizationError — “Incorrect authorization in Isolated” — with the precondition that an IWA declaring that scope extension is installed; the diff concretely demonstrates only the misattribution (an unwarranted IWA opener notification). Any privilege or window-management capability an attacker gains from being treated as the IWA opener is (inferred), as the patch does not itself demonstrate escalation beyond the incorrect attribution, and no exploited-in-the-wild status is stated in the advisory.

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/web_applications/isolated_web_apps/window_management/BUILD.gn
  • 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_browsertest.cc

Audit Directions

  • Default scope-extension inclusion
    Audit every other caller of FindBestAppWithUrlInScope (and similar scope-lookup APIs) that omits exclude_scope_extensions, since the permissive default silently treats extension origins as the app for authorization decisions.
  • Opener/initiator attribution
    Review code that maps an initiator_origin or source_site_instance to an app identity for authorization or notification, ensuring the mapping distinguishes the app’s own origin from externally-controlled scope-extension origins.
  • Scope-extension trust boundaries
    Look for places where being “in scope” (including extended scope) is conflated with “being the app,” especially for IWAs where scope extensions bring in origins outside the isolated isolated-app:// origin.
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/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
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",
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