Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in WebAppInstalls
DescriptionIncorrect authorization in WebAppInstalls
ComponentWebAppInstalls
Bug ClassLogic Error
Tracker502986244
Fix commitfff02dfccecc (chromium/src) +38/-3
CISA KEVNot listed
CreditedGoogle
Disclosed2026-09-08

Changed Functions

FunctionChangeNotes
if
content/browser/renderer_host/isolated_web_app_throttle.cc
modified
TEST_F
content/browser/renderer_host/isolated_web_app_throttle_unittest.cc
modified

Files Changed

  • content/browser/renderer_host/isolated_web_app_throttle.cc
  • content/browser/renderer_host/isolated_web_app_throttle_unittest.cc
From fff02dfcceccf3d62c6e31b8841176a926317aa4 Mon Sep 17 00:00:00 2001
From: greengrape <greengrape@google.com>
Date: Tue, 28 Jul 2026 12:27:32 -0700
Subject: [PATCH] Fix isolated web app throttle for iframes

A compromised Isolated Web App (IWA) renderer could drive iframe navigation requests into any other installed IWA. 

This updates content::IsolatedWebAppThrottle::MaybeThrottleNavigationTransition to correctly handle iframe navigations to other IWAs, preventing them from bypassing apps isolation.

Bug: 502986244
TAG=agy

Change-Id: I1499c81dc446ddf5b76249ec25e57a406a6a6964
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8158080
Commit-Queue: Andrew Rayskiy <greengrape@google.com>
Reviewed-by: Alex Moshchuk <alexmos@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1669655}
---

diff --git a/content/browser/renderer_host/isolated_web_app_throttle.cc b/content/browser/renderer_host/isolated_web_app_throttle.cc
index 96c9f35d..0cf236b 100644
--- a/content/browser/renderer_host/isolated_web_app_throttle.cc
+++ b/content/browser/renderer_host/isolated_web_app_throttle.cc
@@ -233,9 +233,12 @@
     // Handle iframe navigations.
     CHECK(!navigation_handle()->IsInMainFrame());
 
-    // Iframes are allowed to leave the app's origin.
+    // Iframes are allowed to leave the app's origin, but not to navigate
+    // into a different Isolated Web App.
     if (dest_tuple != web_contents_isolation_tuple) {
-      return ThrottleAction::PROCEED;
+      return IsNavigatingToIsolatedApplication(navigation_handle())
+                 ? block_action
+                 : ThrottleAction::PROCEED;
     }
 
     // Block renderer-initiated iframe navigations into the app that were
diff --git a/content/browser/renderer_host/isolated_web_app_throttle_unittest.cc b/content/browser/renderer_host/isolated_web_app_throttle_unittest.cc
index fe16716..c50eb0e4 100644
--- a/content/browser/renderer_host/isolated_web_app_throttle_unittest.cc
+++ b/content/browser/renderer_host/isolated_web_app_throttle_unittest.cc
@@ -36,6 +36,7 @@
 
 const char kAppUrl[] = "https://isolated.app";
 const char kAppUrl2[] = "https://isolated.app/page";
+const char kOtherAppUrl[] = "https://other-isolated.app";
 const char kNonAppUrl[] = "https://example.com";
 const char kNonAppUrl2[] = "https://example.com/page";
 static constexpr WebExposedIsolationLevel kNotIsolated =
@@ -47,7 +48,8 @@
  public:
   bool ShouldUrlUseApplicationIsolationLevel(BrowserContext* browser_context,
                                              const GURL& url) override {
-    return url.GetHost() == GURL(kAppUrl).GetHost();
+    return url.GetHost() == GURL(kAppUrl).GetHost() ||
+           url.GetHost() == GURL(kOtherAppUrl).GetHost();
   }
 
   bool HandleExternalProtocol(
@@ -331,6 +333,36 @@
   CommitRendererInitiatedNavigation(iframe_id, kNonAppUrl, corp_coep_headers());
 }
 
+TEST_F(IsolatedWebAppThrottleTest, BlockIframeNavigationToOtherIsolatedWebApp) {
+  CommitBrowserInitiatedNavigation(kAppUrl, coop_coep_headers());
+  EXPECT_EQ(kIsolatedApplication, GetWebExposedIsolationLevel(main_frame_id()));
+  FrameTreeNodeId iframe_id = CreateIframe(main_frame_id(), "test_frame");
+
+  // Navigating an iframe to a different Isolated Web App should be blocked.
+  auto simulator = StartRendererInitiatedNavigation(iframe_id, kOtherAppUrl);
+
+  auto start_result = simulator->GetLastThrottleCheckResult();
+  EXPECT_EQ(NavigationThrottle::BLOCK_REQUEST, start_result.action());
+}
+
+TEST_F(IsolatedWebAppThrottleTest, BlockIframeRedirectToOtherIsolatedWebApp) {
+  CommitBrowserInitiatedNavigation(kAppUrl, coop_coep_headers());
+  EXPECT_EQ(kIsolatedApplication, GetWebExposedIsolationLevel(main_frame_id()));
+  FrameTreeNodeId iframe_id = CreateIframe(main_frame_id(), "test_frame");
+
+  auto simulator = StartRendererInitiatedNavigation(iframe_id, kNonAppUrl);
+
+  auto start_result = simulator->GetLastThrottleCheckResult();
+  EXPECT_EQ(NavigationThrottle::PROCEED, start_result.action());
+
+  // Redirect to a different Isolated Web App.
+  simulator->SetRedirectHeaders(corp_coep_headers());
+  simulator->Redirect(GURL(kOtherAppUrl));
+
+  auto redirect_result = simulator->GetLastThrottleCheckResult();
+  EXPECT_EQ(NavigationThrottle::BLOCK_REQUEST, redirect_result.action());
+}
+
 TEST_F(IsolatedWebAppThrottleTest,
        BlockIframeRendererInitiatedNavigationIntoIsolatedWebApp) {
   CommitBrowserInitiatedNavigation(kAppUrl, coop_coep_headers());
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/renderer_host/isolated_web_app_throttle_unittest.cc b/content/browser/renderer_host/isolated_web_app_throttle_unittest.cc
index fe16716..c50eb0e4 100644
--- a/content/browser/renderer_host/isolated_web_app_throttle_unittest.cc
+++ b/content/browser/renderer_host/isolated_web_app_throttle_unittest.cc
@@ -36,6 +36,7 @@
 
 const char kAppUrl[] = "https://isolated.app";
 const char kAppUrl2[] = "https://isolated.app/page";
+const char kOtherAppUrl[] = "https://other-isolated.app";
 const char kNonAppUrl[] = "https://example.com";
 const char kNonAppUrl2[] = "https://example.com/page";
 static constexpr WebExposedIsolationLevel kNotIsolated =
@@ -47,7 +48,8 @@
  public:
   bool ShouldUrlUseApplicationIsolationLevel(BrowserContext* browser_context,
                                              const GURL& url) override {
-    return url.GetHost() == GURL(kAppUrl).GetHost();
+    return url.GetHost() == GURL(kAppUrl).GetHost() ||
+           url.GetHost() == GURL(kOtherAppUrl).GetHost();
   }
 
   bool HandleExternalProtocol(
@@ -331,6 +333,36 @@
   CommitRendererInitiatedNavigation(iframe_id, kNonAppUrl, corp_coep_headers());
 }
 
+TEST_F(IsolatedWebAppThrottleTest, BlockIframeNavigationToOtherIsolatedWebApp) {
+  CommitBrowserInitiatedNavigation(kAppUrl, coop_coep_headers());
+  EXPECT_EQ(kIsolatedApplication, GetWebExposedIsolationLevel(main_frame_id()));
+  FrameTreeNodeId iframe_id = CreateIframe(main_frame_id(), "test_frame");
+
+  // Navigating an iframe to a different Isolated Web App should be blocked.
+  auto simulator = StartRendererInitiatedNavigation(iframe_id, kOtherAppUrl);
+
+  auto start_result = simulator->GetLastThrottleCheckResult();
+  EXPECT_EQ(NavigationThrottle::BLOCK_REQUEST, start_result.action());
+}
+
+TEST_F(IsolatedWebAppThrottleTest, BlockIframeRedirectToOtherIsolatedWebApp) {
+  CommitBrowserInitiatedNavigation(kAppUrl, coop_coep_headers());
+  EXPECT_EQ(kIsolatedApplication, GetWebExposedIsolationLevel(main_frame_id()));
+  FrameTreeNodeId iframe_id = CreateIframe(main_frame_id(), "test_frame");
+
+  auto simulator = StartRendererInitiatedNavigation(iframe_id, kNonAppUrl);
+
+  auto start_result = simulator->GetLastThrottleCheckResult();
+  EXPECT_EQ(NavigationThrottle::PROCEED, start_result.action());
+
+  // Redirect to a different Isolated Web App.
+  simulator->SetRedirectHeaders(corp_coep_headers());
+  simulator->Redirect(GURL(kOtherAppUrl));
+
+  auto redirect_result = simulator->GetLastThrottleCheckResult();
+  EXPECT_EQ(NavigationThrottle::BLOCK_REQUEST, redirect_result.action());
+}
+
 TEST_F(IsolatedWebAppThrottleTest,
        BlockIframeRendererInitiatedNavigationIntoIsolatedWebApp) {
   CommitBrowserInitiatedNavigation(kAppUrl, coop_coep_headers());
Loading diff…

Original Bug Report

reported by vm...@google.com

Cross-IWA iframe navigation enables Service Worker CSRF and installation oracle

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 go/chrome-ai-generated-security-bugs-faq for more information.

Overview: A logic flaw in IsolatedWebAppThrottle fails to restrict cross-origin iframe navigations between Isolated Web Apps (IWAs). A compromised IWA renderer can exploit this, bypassing CSP checks, to dispatch arbitrary FetchEvents to other IWAs’ Service Workers (CSRF) or probe their installation status.

Affected files:

  • content/browser/renderer_host/isolated_web_app_throttle.cc
  • chrome/browser/web_applications/isolated_web_apps/isolated_web_app_throttle.cc
  • content/browser/renderer_host/navigation_request.cc
  • components/webapps/isolated_web_apps/url_loading/url_loader_factory.cc
  • content/browser/child_process_security_policy_impl.cc
  • chrome/browser/web_applications/isolated_web_apps/chrome_iwa_client.cc
  • components/webapps/isolated_web_apps/url_loading/utils.cc

Estimated timestamp from git blame: 2025-05-15

Summary

A compromised Isolated Web App (IWA) renderer can drive iframe navigation requests into any other installed IWA. This occurs because the IsolatedWebAppThrottle implementation for iframes incorrectly allows navigations to any isolated-app:// destination if it differs from the current app’s origin, without verifying if the destination is another IWA that should be isolated. By bypassing browser-side CSP checks via IPC manipulation, an attacker can trigger a cross-IWA Service Worker FetchEvent (blind CSRF) and utilize error message leakage as an installation oracle.

Vulnerability Details

  1. Broad Scheme Access: When an IWA is launched, ChildProcessSecurityPolicyImpl::GrantCommitURL grants the renderer process request access to the entire non-web-safe isolated-app scheme (content/browser/child_process_security_policy_impl.cc:1378). Thus, CanRequestURL permits the compromised renderer to request URLs belonging to other IWAs.
  2. CSP Bypass: The attacker initiates an iframe navigation to a target IWA (isolated-app://<B-id>/) and sets should_check_main_world_csp to DO_NOT_CHECK in the mojom::LocalFrameHost::BeginNavigation IPC. This causes NavigationRequest::CheckContentSecurityPolicy to immediately return net::OK (content/browser/renderer_host/navigation_request.cc:7698), bypassing the frame-src 'self' policy that normally prevents cross-IWA embedding.
  3. Throttle Logic Flaw: The navigation reaches content::IsolatedWebAppThrottle::MaybeThrottleNavigationTransition. For iframe navigations, the throttle checks if (dest_tuple != web_contents_isolation_tuple) (content/browser/renderer_host/isolated_web_app_throttle.cc:237). If true, it returns ThrottleAction::PROCEED. It mistakenly assumes the navigation is leaving the IWA for the open web, failing to check if the destination is another IWA (lacking the !dest_needs_apps_isolation check present in the main-frame branch).

Impact

Although the navigation response is eventually blocked by AncestorThrottle (due to headers injected later in the pipeline), two critical side effects occur first:

  1. Service Worker CSRF: The browser resolves the navigation to the target IWA’s dedicated StoragePartition. If the target is installed, ServiceWorkerMainResourceLoaderInterceptor dispatches a FetchEvent to its Service Worker with an attacker-controlled URL. This enables blind CSRF against any target IWA.
  2. Installation Oracle: If the target IWA is not installed, the IsolatedWebAppURLLoaderFactory fails to find the app bundle. It attempts to log “There’s no matching Isolated Web App installed.” to the console. Because the iframe navigation is still pending, LogErrorMessageToConsole sends this error via IPC to the RenderFrameHost currently hosting the frame, which is still controlled by the attacker. The attacker can observe this IPC to deterministically leak the installation status of any IWA bundle ID.

Potential Steps to Reproduce

Note: These are suggested steps based on codebase analysis; we do not currently have the tooling to execute a working proof of concept.

  1. Install two IWAs (App A and App B). Ensure App B registers a Service Worker.
  2. Gain arbitrary code execution in App A’s renderer process.
  3. From the compromised renderer, construct a mojom::LocalFrameHost::BeginNavigation IPC for a child frame targeting isolated-app://<App-B-ID>/victim?data=attacker.
  4. Set should_check_main_world_csp in the CommonNavigationParams to network::mojom::CSPDisposition::DO_NOT_CHECK.
  5. Send the IPC. Observe that App B’s Service Worker receives the FetchEvent.
  6. Uninstall App B and repeat the process. Observe the “no matching Isolated Web App” error message IPC arriving at the attacker’s renderer.

Suggested Fix

Update content::IsolatedWebAppThrottle::MaybeThrottleNavigationTransition to correctly handle iframe navigations to other IWAs. Specifically, at line 237:

// Iframes are allowed to leave the app's origin, but NOT to enter another IWA.
if (dest_tuple != web_contents_isolation_tuple) {
  return dest_needs_apps_isolation ? ThrottleAction::CANCEL : ThrottleAction::PROCEED;
}

This mirrors the protection applied to main-frame navigations and will block the transition before it reaches the StoragePartition or Service Worker layers.

Evaluated with Chrome root at commit: 661452647ddb2827305122ff3273bd5dea403f09


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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