Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Isolated Web Apps
DescriptionInappropriate implementation in Isolated Web Apps
ComponentIsolated Web Apps
Bug ClassLogic Error
Tracker500468338
Fix commit096fc4161b13 (chromium/src) +100/-36
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
if
chrome/browser/web_applications/isolated_web_apps/commands/isolated_web_app_apply_update_command.cc
modified
if
components/webapps/isolated_web_apps/identity/iwa_identity_validator.cc
modified

Files Changed

  • chrome/browser/web_applications/isolated_web_apps/commands/isolated_web_app_apply_update_command.cc
  • components/webapps/isolated_web_apps/bundle_operations/bundle_operations.cc
  • components/webapps/isolated_web_apps/identity/iwa_identity_validator.cc
  • components/webapps/isolated_web_apps/identity/iwa_identity_validator.h
From 096fc4161b13a1a13c00ebde71a0329c37793d60 Mon Sep 17 00:00:00 2001
From: greengrape <greengrape@google.com>
Date: Thu, 16 Apr 2026 07:52:58 -0700
Subject: [PATCH] [IWA] Enforce strict signature validation during installs and updates

The Isolated Web App (IWA) key rotation mechanism was previously too
permissive, unconditionally accepting compromised "previous" keys
during fresh installations and updates.

This CL:
1. Refactors IwaIdentityValidator to be context-aware, adding a static
   ValidateWebBundleIdentity method that accepts an
   allow_soft_key_rotation flag.
2. Updates IWA-specific validation paths (installs, updates, metadata
   reading) to use strict validation (allow_soft_key_rotation = false).
3. Maintains soft rotation (allow_soft_key_rotation = true) for runtime
   resource loading to allow existing installations to run during the
   grace period.
4. Strengthens IsolatedWebAppApplyUpdateCommand to enforce key rotation
   for all updates (including version increases) when a rotation is
   pending. Explicitly documents the update state machine.
5. Removes redundant browser tests that were improperly testing internal
   heuristic states. Verified that core security enforcement remains
   intact and all relevant tests pass.
6. Redirects IsolatedWebAppValidator to use the new static method in
   IwaIdentityValidator instead of calling web_package::IdentityValidator
   directly, and cleans up the redundant include.
7. Adds documentation and links to go/iwa-soft-key-rotation in relevant
   validation paths.

Bug: 500468338
Change-Id: I70b12b2c8785e87b7c6862ce5a27db006a6a6964
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7748359
Reviewed-by: Robert Ferens <rferens@google.com>
Commit-Queue: Andrew Rayskiy <greengrape@google.com>
Cr-Commit-Position: refs/heads/main@{#1615857}
---

diff --git a/chrome/browser/web_applications/isolated_web_apps/commands/isolated_web_app_apply_update_command.cc b/chrome/browser/web_applications/isolated_web_apps/commands/isolated_web_app_apply_update_command.cc
index 472014e..5b598cb 100644
--- a/chrome/browser/web_applications/isolated_web_apps/commands/isolated_web_app_apply_update_command.cc
+++ b/chrome/browser/web_applications/isolated_web_apps/commands/isolated_web_app_apply_update_command.cc
@@ -153,28 +153,46 @@
   const IwaVersion& pending_version = pending_update_info().version;
   const IwaVersion& current_version = isolation_data().version();
 
+  std::optional<KeyRotationData> kr_data =
+      GetKeyRotationData(url_info_.web_bundle_id(), isolation_data());
+  if (kr_data) {
+    GetMutableDebugValue().Set("rotated_key",
+                               base::Base64Encode(kr_data->rotated_key));
+  }
+
+  // 1. If a key rotation is announced, the update must contain the rotated key.
+  // Exit early if we know that the bundle cannot be installed.
+  if (kr_data && !kr_data->pending_update_has_rk) {
+    ReportFailure(base::StringPrintf(
+        "The update for version %s does not contain the rotated key.",
+        pending_version.GetString().c_str()));
+    return;
+  }
+
+  // 2. Handle version-based update logic.
   if (pending_version > current_version) {
+    // Standard update: the new version is strictly greater.
     std::move(next_step_callback).Run();
   } else if (pending_version < current_version) {
-    // Downgrade: Remove user data to avoid incompatibility. Proceed with
-    // update.
+    // Downgrade: The candidate version is older than the current one.
+    // Proceed with the update, but remove user data first to avoid
+    // incompatibility.
     web_app::RemoveIsolatedWebAppBrowsingData(&profile(), url_info_.origin(),
                                               std::move(next_step_callback));
   } else {
-    // Handle key rotation for same-version updates.
-    if (auto kr_data =
-            GetKeyRotationData(url_info_.web_bundle_id(), isolation_data())) {
-      GetMutableDebugValue().Set("rotated_key",
-                                 base::Base64Encode(kr_data->rotated_key));
-      if (!kr_data->current_installation_has_rk &&
-          kr_data->pending_update_has_rk) {
-        std::move(next_step_callback).Run();
-        return;
-      }
+    // Same-version update: This is only allowed if it fulfills a required
+    // key rotation.
+    if (kr_data && !kr_data->current_installation_has_rk) {
+      // Rotation fulfillment: same version, but moving the app from the
+      // compromised key to the rotated key.
+      CHECK(kr_data->pending_update_has_rk);
+      std::move(next_step_callback).Run();
+      return;
     }
-    // No version change, and no key rotation needed.
+
+    // Otherwise, there's no reason to apply an update for the same version.
     ReportFailure(base::StringPrintf("Installed app is already on version %s.",
-                                     current_version.GetString()));
+                                     current_version.GetString().c_str()));
   }
 }
 
diff --git a/components/webapps/isolated_web_apps/bundle_operations/bundle_operations.cc b/components/webapps/isolated_web_apps/bundle_operations/bundle_operations.cc
index ff02c38..d92a08d 100644
--- a/components/webapps/isolated_web_apps/bundle_operations/bundle_operations.cc
+++ b/components/webapps/isolated_web_apps/bundle_operations/bundle_operations.cc
@@ -51,11 +51,15 @@
         std::move(callback).Run(base::unexpected(error.ToString()));
       });
 
+  // Validate that the bundle's identity matches its ID. Soft key rotation is
+  // not supported here because this function is used for IWA modification
+  // operations (installs, updates, etc.), which must always use the most
+  // up-to-date keys. See go/iwa-soft-key-rotation for more details.
   auto validation_result =
       IsolatedWebAppValidator::ValidateIntegrityBlockAndMetadata(
           browser_context.get(), expected_web_bundle_id,
           reader->GetIntegrityBlock(), reader->GetPrimaryURL(),
-          reader->GetEntries());
+          reader->GetEntries(), /*allow_soft_key_rotation=*/false);
   UmaLogExpectedStatus("WebApp.Isolated.SwbnFileUsability", validation_result);
 
   IntegrityBlockResult integrity_block_result =
diff --git a/components/webapps/isolated_web_apps/identity/iwa_identity_validator.cc b/components/webapps/isolated_web_apps/identity/iwa_identity_validator.cc
index 095eca33..eb86580 100644
--- a/components/webapps/isolated_web_apps/identity/iwa_identity_validator.cc
+++ b/components/webapps/isolated_web_apps/identity/iwa_identity_validator.cc
@@ -22,11 +22,12 @@
 using KeyRotationInfo = IwaRuntimeDataProvider::KeyRotationInfo;
 
 bool Matches(const web_package::PublicKey& public_key,
-             const KeyRotationInfo& kr_info) {
+             const KeyRotationInfo& kr_info,
+             bool allow_soft_key_rotation) {
   return std::visit(
       [&](const auto& public_key) {
         return std::ranges::equal(public_key.bytes(), kr_info.public_key) ||
-               (kr_info.previous_key &&
+               (allow_soft_key_rotation && kr_info.previous_key &&
                 std::ranges::equal(public_key.bytes(), *kr_info.previous_key));
       },
       public_key);
@@ -36,9 +37,10 @@
 ValidateWebBundleIdentityAgainstKeyRotationInfo(
     const std::string& web_bundle_id,
     const std::vector<web_package::PublicKey>& public_keys,
-    const KeyRotationInfo& kr_info) {
+    const KeyRotationInfo& kr_info,
+    bool allow_soft_key_rotation) {
   if (std::ranges::any_of(public_keys, [&](const auto& public_key) {
-        return Matches(public_key, kr_info);
+        return Matches(public_key, kr_info, allow_soft_key_rotation);
       })) {
     return base::ok();
   }
@@ -64,7 +66,8 @@
           IwaClient::GetInstance()->GetRuntimeDataProvider()) {
     if (const auto* kr_info = provider->GetKeyRotationInfo(web_bundle_id)) {
       return ValidateWebBundleIdentityAgainstKeyRotationInfo(
-          web_bundle_id, public_keys, *kr_info);
+          web_bundle_id, public_keys, *kr_info,
+          /*allow_soft_key_rotation=*/true);
     }
   }
 
@@ -72,4 +75,22 @@
                                                       public_keys);
 }
 
+// static
+base::expected<void, std::string>
+IwaIdentityValidator::ValidateWebBundleIdentity(
+    const std::string& web_bundle_id,
+    const std::vector<web_package::PublicKey>& public_keys,
+    bool allow_soft_key_rotation) {
+  if (const auto* provider =
+          IwaClient::GetInstance()->GetRuntimeDataProvider()) {
+    if (const auto* kr_info = provider->GetKeyRotationInfo(web_bundle_id)) {
+      return ValidateWebBundleIdentityAgainstKeyRotationInfo(
+          web_bundle_id, public_keys, *kr_info, allow_soft_key_rotation);
+    }
+  }
+
+  return web_package::IdentityValidator::GetInstance()
+      ->ValidateWebBundleIdentity(web_bundle_id, public_keys);
+}
+
 }  // namespace web_app
diff --git a/components/webapps/isolated_web_apps/identity/iwa_identity_validator.h b/components/webapps/isolated_web_apps/identity/iwa_identity_validator.h
index cc292c8..856572c 100644
--- a/components/webapps/isolated_web_apps/identity/iwa_identity_validator.h
+++ b/components/webapps/isolated_web_apps/identity/iwa_identity_validator.h
@@ -21,6 +21,15 @@
       const std::string& web_bundle_id,
       const std::vector<web_package::PublicKey>& public_keys) const override;
 
+  // Same as above, but allows disabling "soft" key rotation (i.e. accepting
+  // "previous" keys that are still trusted for execution, but shouldn't be used
+  // for fresh installs or updates).
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/components/webapps/isolated_web_apps/reading/validator_unittest.cc b/components/webapps/isolated_web_apps/reading/validator_unittest.cc
index 34ce424..c9293d9 100644
--- a/components/webapps/isolated_web_apps/reading/validator_unittest.cc
+++ b/components/webapps/isolated_web_apps/reading/validator_unittest.cc
@@ -106,7 +106,7 @@
 
   EXPECT_THAT(IsolatedWebAppValidator::ValidateIntegrityBlock(
                   &browser_context_, test::GetDefaultEcdsaP256WebBundleId(),
-                  integrity_block),
+                  integrity_block, /*allow_soft_key_rotation=*/false),
               UnusableSwbnErrorIs(Error::kIntegrityBlockValidationError,
                                   "does not match the expected Web Bundle ID"));
 }
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential IWA Key Rotation Bypass via Unconditional Acceptance of Compromised Previous Key

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 security team.

Overview: The Isolated Web App (IWA) key rotation mechanism can be bypassed because the identity validator unconditionally accepts compromised keys during the grace period. Additionally, key rotation enforcement is skipped for version-increasing updates, and the update discovery download heuristic is easily circumvented by padding. An attacker with a compromised key can exploit this to push malicious updates and retain access to highly privileged IWA APIs.

Affected files:

  • components/webapps/isolated_web_apps/identity/iwa_identity_validator.cc
  • chrome/browser/web_applications/isolated_web_apps/commands/isolated_web_app_apply_update_command.cc
  • chrome/browser/web_applications/isolated_web_apps/update/isolated_web_app_update_discovery_task.cc
  • chrome/browser/web_applications/isolated_web_apps/commands/install_isolated_web_app_command.cc

Estimated timestamp from git blame: 2026-02-06

The Isolated Web App (IWA) key rotation feature is designed to handle compromised developer signing keys by transitioning trust to a new key ($K_2$) while allowing the compromised key ($K_1$) a grace period to only execute already-installed apps. However, a combination of logic flaws allows an attacker holding the compromised key to bypass the rotation mechanism and push malicious updates.

1. Unconditional acceptance of previous_key in IwaIdentityValidator

In components/webapps/isolated_web_apps/identity/iwa_identity_validator.cc, the Matches function verifies signatures against KeyRotationInfo. It returns true if the key matches either the expected_key ($K_2$) or the previous_key ($K_1$). This validator is invoked during the signature verification path for both new installations and updates (via IsolatedWebAppInstallCommandHelper::CheckTrustAndSignatures). Because it does not check the operation context, a malicious update bundle signed only with the compromised $K_1$ will pass identity validation.

2. Key rotation check bypass for version-increasing updates

In chrome/browser/web_applications/isolated_web_apps/commands/isolated_web_app_apply_update_command.cc, the HandleKeyRotationOrDowngradeIfNecessary function enforces key rotation logic only when the update version is the same as the current version. If pending_version > current_version, the command proceeds immediately without verifying that the update actually contains the expected_key (via pending_update_has_rk). An attacker can simply increment the version number in their malicious bundle to bypass this final enforcement check.

3. Bypassable discovery heuristic

IsolatedWebAppUpdateDiscoveryTask::CheckIntegrityBundleForRotatedKey downloads the first 8 KiB of a bundle to heuristically check for the rotated key. It assumes the integrity block fits within this chunk by checking for two 📦 markers (initial_bytes->rfind("📦") != initial_bytes->find("📦")). If an attacker pads the integrity block such that only one marker falls within the first 8 KiB, this condition evaluates to false, the heuristic is bypassed, and the full malicious bundle is downloaded and staged.

Potential Attack Steps

Note: These are suggested steps based on static analysis; our tooling agent does not have the ability to run code to verify them dynamically.

  1. The attacker holds a compromised IWA signing key ($K_1$) for an app currently installed on a victim’s device at version $V_1$.
  2. The legitimate developer rotates the key via the KDC, setting the expected key to $K_2$ and the previous key to $K_1$.
  3. The attacker creates a malicious IWA update bundle signed only with $K_1$, and sets its version to $V_2$ (where $V_2 > V_1$).
  4. The attacker pads the integrity block of the bundle to exceed 8 KiB.
  5. The victim’s Chrome instance discovers the update. The 8 KiB download heuristic is bypassed because the padding pushes the second 📦 marker out of the initial chunk.
  6. The update is fully downloaded and staged. Signature validation succeeds because IwaIdentityValidator unconditionally accepts the compromised $K_1$.
  7. During update application, IsolatedWebAppApplyUpdateCommand skips key rotation enforcement because the attacker chose a version $V_2 > V_1$.
  8. The malicious update is installed over the legitimate app, granting the attacker’s code access to privileged isolated-app:// APIs (e.g., DIRECT_SOCKETS).

Suggested Fixes

  1. IwaIdentityValidator Context Awareness: Modify the validator to reject the previous_key during installation and update flows. It should only be accepted for the execution of already-installed legacy versions.
  2. Enforce Rotation on Version Increases: Update IsolatedWebAppApplyUpdateCommand::HandleKeyRotationOrDowngradeIfNecessary to enforce the presence of the rotated key (kr_data->pending_update_has_rk) even when pending_version > current_version, provided a rotation is actively pending (!kr_data->current_installation_has_rk).
  3. Robust Integrity Block Parsing: Remove the fragile 8 KiB string-search heuristic in IsolatedWebAppUpdateDiscoveryTask. Instead, securely parse the integrity block’s length from the bundle header and download exactly the bytes needed to validate the presence of the rotated key.

Evaluated with Chrome root at commit: 137d451a126685dd5010e6609db9f6d4a78d8234


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