CVE-2026-87580
Overview
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
contentnavigation throttle that inspects each navigation transition and decides whether it mayPROCEEDor 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.
IsNavigatingToIsolatedApplication guard so navigations targeting another IWA are blocked instead of proceeding.Attack Path
- 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).
- 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_tuplediffers fromweb_contents_isolation_tuple. - Bypass the throttle
The pre-patch iframe branch matches
dest_tuple != web_contents_isolation_tupleand returnsThrottleAction::PROCEED, allowing the navigation into the other IWA that isolation policy should have blocked. - 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).
- 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
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
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/renderer_host/isolated_web_app_throttle.cc |
modified | |
TEST_Fcontent/browser/renderer_host/isolated_web_app_throttle_unittest.cc |
modified |
Files Changed
content/browser/renderer_host/isolated_web_app_throttle.cccontent/browser/renderer_host/isolated_web_app_throttle_unittest.cc
Audit Directions
- Permissive "leaving the origin" allowancesAudit navigation throttles and origin checks that grant a blanket
PROCEEDfor 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 asymmetryReview 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-checksConfirm 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.
Patch
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());
Regression Test / PoC
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());
Original Bug Report
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.ccchrome/browser/web_applications/isolated_web_apps/isolated_web_app_throttle.cccontent/browser/renderer_host/navigation_request.cccomponents/webapps/isolated_web_apps/url_loading/url_loader_factory.cccontent/browser/child_process_security_policy_impl.ccchrome/browser/web_applications/isolated_web_apps/chrome_iwa_client.cccomponents/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
- Broad Scheme Access: When an IWA is launched,
ChildProcessSecurityPolicyImpl::GrantCommitURLgrants the renderer process request access to the entire non-web-safeisolated-appscheme (content/browser/child_process_security_policy_impl.cc:1378). Thus,CanRequestURLpermits the compromised renderer to request URLs belonging to other IWAs. - CSP Bypass: The attacker initiates an iframe navigation to a target IWA (
isolated-app://<B-id>/) and setsshould_check_main_world_csptoDO_NOT_CHECKin themojom::LocalFrameHost::BeginNavigationIPC. This causesNavigationRequest::CheckContentSecurityPolicyto immediately returnnet::OK(content/browser/renderer_host/navigation_request.cc:7698), bypassing theframe-src 'self'policy that normally prevents cross-IWA embedding. - Throttle Logic Flaw: The navigation reaches
content::IsolatedWebAppThrottle::MaybeThrottleNavigationTransition. For iframe navigations, the throttle checksif (dest_tuple != web_contents_isolation_tuple)(content/browser/renderer_host/isolated_web_app_throttle.cc:237). If true, it returnsThrottleAction::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_isolationcheck 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:
- Service Worker CSRF: The browser resolves the navigation to the target IWA’s dedicated
StoragePartition. If the target is installed,ServiceWorkerMainResourceLoaderInterceptordispatches aFetchEventto its Service Worker with an attacker-controlled URL. This enables blind CSRF against any target IWA. - Installation Oracle: If the target IWA is not installed, the
IsolatedWebAppURLLoaderFactoryfails 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,LogErrorMessageToConsolesends this error via IPC to theRenderFrameHostcurrently 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.
- Install two IWAs (App A and App B). Ensure App B registers a Service Worker.
- Gain arbitrary code execution in App A’s renderer process.
- From the compromised renderer, construct a
mojom::LocalFrameHost::BeginNavigationIPC for a child frame targetingisolated-app://<App-B-ID>/victim?data=attacker. - Set
should_check_main_world_cspin theCommonNavigationParamstonetwork::mojom::CSPDisposition::DO_NOT_CHECK. - Send the IPC. Observe that App B’s Service Worker receives the
FetchEvent. - 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.