Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in WebAppInstalls
DescriptionIncorrect authorization in WebAppInstalls
ComponentWebAppInstalls
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.
Tracker502986244
Fix commitfff02dfccecc (chromium/src) +38/-3
CWECWE-863
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-04-15 (per vendor advisory)
Fix landed2026-07-28 (public git)
Disclosed2026-09-08
Patch gap42 days — fix public in source before the advisory

Background

Isolated Web App (IWA)
a specially packaged, signed web app that Chrome grants a hardened, application-level isolation context with strict cross-origin boundaries between distinct IWAs.
`IsolatedWebAppThrottle`
a content navigation throttle that inspects each navigation transition and decides whether it may PROCEED or must be blocked to preserve IWA isolation invariants.
Isolation tuple
the site/origin isolation key associated with a WebContents (web_contents_isolation_tuple) versus the destination of a navigation (dest_tuple), compared to detect origin transitions.
Renderer-initiated navigation
a navigation triggered from within page content (e.g. an iframe changing its src), which a compromised renderer can control and therefore cannot be trusted to respect isolation.

Root Cause Analysis

The vulnerable path is the iframe branch of IsolatedWebAppThrottle::MaybeThrottleNavigationTransition (function named in the commit message; the enclosing function is not shown in the hunk context). For a subframe navigation where the destination isolation tuple differed from the WebContents tuple (dest_tuple != web_contents_isolation_tuple), the throttle unconditionally returned ThrottleAction::PROCEED, on the stated assumption that “iframes are allowed to leave the app’s origin.” That assumption conflated “leaving the current app’s origin” with “going to a non-app destination,” so it also permitted an iframe to navigate into a different Isolated Web App, violating the invariant that one IWA context must never host or reach another IWA’s application-isolated content.

The fix tightens the check: before proceeding it now calls IsNavigatingToIsolatedApplication(navigation_handle()) and returns block_action when the destination is itself an IWA, only returning PROCEED for genuinely non-app destinations. The shipped tests confirm both the initial-request case (BlockIframeNavigationToOtherIsolatedWebApp) and the server-redirect case (BlockIframeRedirectToOtherIsolatedWebApp) are now blocked while ordinary cross-origin navigation to non-app URLs still proceeds.

Key insight
The single mistake was treating any cross-tuple iframe navigation as “leaving the app” and allowing it, without distinguishing a non-app destination from a different IWA destination; the fix adds an IsNavigatingToIsolatedApplication guard so navigations targeting another IWA are blocked instead of proceeding.

Attack Path

  1. Compromise an IWA renderer An attacker gains code execution in the renderer process of an already-installed Isolated Web App (the throttle explicitly must not trust renderer-initiated navigations).
  2. Point a subframe at another IWA From the compromised renderer, initiate a renderer-initiated iframe navigation whose destination is a different installed IWA’s URL, so dest_tuple differs from web_contents_isolation_tuple.
  3. Bypass the throttle The pre-patch iframe branch matches dest_tuple != web_contents_isolation_tuple and returns ThrottleAction::PROCEED, allowing the navigation into the other IWA that isolation policy should have blocked.
  4. Cross app isolation The iframe loads the victim IWA’s application-isolated content inside the attacker’s IWA context, breaching the boundary between separate Isolated Web Apps (an incorrect-authorization / apps-isolation bypass per the advisory).
  5. Redirect variant The same bypass is reachable when a navigation to a non-app URL is server-redirected into an IWA, which the fix also blocks via the redirect throttle check.

Impact Assessment

An attacker with a compromised IWA renderer gains the ability to drive iframe navigations into any other installed Isolated Web App, defeating the cross-IWA isolation boundary in the browser-enforced navigation throttle (incorrect authorization in WebAppInstalls, per the advisory). The precondition is an already-compromised or malicious IWA renderer capable of issuing renderer-initiated (or redirect-driven) subframe navigations. Any further escalation to reading the victim app’s data or executing in its context is (inferred) and not demonstrated by the patch, which only shows the navigation itself being blocked.

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

Audit Directions

  • Permissive "leaving the origin" allowances
    Audit navigation throttles and origin checks that grant a blanket PROCEED for cross-origin/cross-tuple transitions; verify they distinguish non-privileged destinations from privileged ones (IWAs, WebUI, extensions) rather than allowing all “external” targets.
  • Iframe vs. main-frame asymmetry
    Review branches that relax checks specifically for subframes; a rule that is safe for top-level navigation (leaving the app) can be unsafe for iframes that can be pointed at sensitive targets by a compromised renderer.
  • Redirect-time re-checks
    Confirm that every isolation/authorization decision made at navigation start is re-evaluated on server redirects, since the shipped redirect test shows the destination class can change mid-navigation.
From fff02dfcceccf3d62c6e31b8841176a926317aa4 Mon Sep 17 00:00:00 2001
From: greengrape <[email protected]>
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 <[email protected]>
Reviewed-by: Alex Moshchuk <[email protected]>
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 [email protected]

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