Low chrome Logic Error 🔧 Commit mapped

Overview

Low
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
Tracker497558336
Fix commit0cae24a265c1 (chromium/src) +140/-57
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
if
chrome/browser/web_applications/commands/add_validated_origin_associations_command.cc
modified
AddValidatedOriginAssociationsCommand
chrome/browser/web_applications/commands/add_validated_origin_associations_command.h
modified
TEST_F
chrome/browser/web_applications/commands/add_validated_origin_associations_command_unittest.cc
modified

Files Changed

  • chrome/browser/web_applications/commands/add_validated_origin_associations_command.cc
  • chrome/browser/web_applications/commands/add_validated_origin_associations_command.h
  • chrome/browser/web_applications/commands/add_validated_origin_associations_command_unittest.cc
From 0cae24a265c166f3d69538fd92ab89d171303778 Mon Sep 17 00:00:00 2001
From: Vlad Krot <[email protected]>
Date: Fri, 15 May 2026 05:40:43 -0700
Subject: [PATCH] [IWA] Make origin associations from server to be source of truth.

This ensures that stale scope extensions in the web app database are
cleared whenever an IWA is launched, if the entries have been removed
from the corresponding web-app-origin-associations file, instead of
persisting them.

Bug: 497558336
Change-Id: I991e31d04949e7d3cbfc12169979a4c611894a61
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7845255
Commit-Queue: Vlad Krot <[email protected]>
Reviewed-by: Dibyajyoti Pal <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1631213}
---

diff --git a/chrome/browser/web_applications/commands/add_validated_origin_associations_command.cc b/chrome/browser/web_applications/commands/add_validated_origin_associations_command.cc
index 85c446b..1f65f4b 100644
--- a/chrome/browser/web_applications/commands/add_validated_origin_associations_command.cc
+++ b/chrome/browser/web_applications/commands/add_validated_origin_associations_command.cc
@@ -67,24 +67,6 @@
     return;
   }
 
-  bool needs_scope_validation =
-      !base::STLSetDifference<base::flat_set<ScopeExtensionInfo>>(
-           app->scope_extensions(), app->validated_scope_extensions())
-           .empty();
-
-  bool needs_migration_validation =
-      !base::STLSetDifference<base::flat_set<MigrationSource>>(
-           base::flat_set<MigrationSource>(
-               app->unvalidated_migration_sources()),
-           base::flat_set<MigrationSource>(app->validated_migration_sources()))
-           .empty();
-
-  if (!needs_scope_validation && !needs_migration_validation) {
-    CompleteAndSelfDestruct(CommandResult::kSuccess,
-                            AddValidatedOriginAssociationsResult::kNotNeeded);
-    return;
-  }
-
   // If the last validation time isn't set, randomize it in the past to ensure
   // no network fetch spikes.
   if (!app->origin_association_last_validation_check_time().has_value()) {
@@ -130,24 +112,17 @@
     web_app::WebApp& app = CHECK_DEREF(update->UpdateApp(app_id_));
 
     {
-      // Gather union of current validated scope extensions and received.
-      auto previously_validated_and_requested =
-          base::STLSetIntersection<base::flat_set<ScopeExtensionInfo>>(
-              app.validated_scope_extensions(), app.scope_extensions());
-
-      auto final_validated =
-          base::STLSetUnion<base::flat_set<ScopeExtensionInfo>>(
-              previously_validated_and_requested,
-              validated_origin_associations.scope_extensions);
-
       unvalidated_items_remain =
           !base::STLSetDifference<base::flat_set<ScopeExtensionInfo>>(
-               app.scope_extensions(), final_validated)
+               app.scope_extensions(),
+               validated_origin_associations.scope_extensions)
                .empty();
 
       scope_extensions_updated =
-          final_validated != app.validated_scope_extensions();
-      app.SetValidatedScopeExtensions(std::move(final_validated));
+          validated_origin_associations.scope_extensions !=
+          app.validated_scope_extensions();
+      app.SetValidatedScopeExtensions(
+          std::move(validated_origin_associations.scope_extensions));
     }
 
     {
@@ -159,23 +134,16 @@
       base::flat_set<MigrationSource> new_validated(
           validated_origin_associations.migration_sources);
 
-      auto previously_validated_and_requested =
-          base::STLSetIntersection<base::flat_set<MigrationSource>>(
-              original_unvalidated, original_validated);
-
-      auto final_validated = base::STLSetUnion<base::flat_set<MigrationSource>>(
-          previously_validated_and_requested, new_validated);
-
       unvalidated_items_remain =
           unvalidated_items_remain ||
           !base::STLSetDifference<base::flat_set<MigrationSource>>(
-               original_unvalidated, final_validated)
+               original_unvalidated, new_validated)
                .empty();
 
       // Check if any migration sources were added or removed.
-      migration_sources_updated = original_validated != final_validated;
+      migration_sources_updated = original_validated != new_validated;
 
-      app.SetValidatedMigrationSources(std::move(final_validated).extract());
+      app.SetValidatedMigrationSources(std::move(new_validated).extract());
     }
     app.SetOriginAssociationLastValidationCheckTime(now_time);
   }
diff --git a/chrome/browser/web_applications/commands/add_validated_origin_associations_command.h b/chrome/browser/web_applications/commands/add_validated_origin_associations_command.h
index 8a7c25b..1b86c37d 100644
--- a/chrome/browser/web_applications/commands/add_validated_origin_associations_command.h
+++ b/chrome/browser/web_applications/commands/add_validated_origin_associations_command.h
@@ -17,6 +17,10 @@
 
 namespace web_app {
 
+// The command revalidates existing origin associations,
+// effectively treating server side values as source of truth,
+// meaning previously validated items might be removed, if server
+// is no longer returning them.
 class AddValidatedOriginAssociationsCommand
     : public WebAppCommand<AppLock, AddValidatedOriginAssociationsResult> {
  public:
diff --git a/chrome/browser/web_applications/commands/add_validated_origin_associations_command_unittest.cc b/chrome/browser/web_applications/commands/add_validated_origin_associations_command_unittest.cc
index 2e9d7b6..5debe34 100644
--- a/chrome/browser/web_applications/commands/add_validated_origin_associations_command_unittest.cc
+++ b/chrome/browser/web_applications/commands/add_validated_origin_associations_command_unittest.cc
@@ -318,22 +318,24 @@
   EXPECT_TRUE(app->origin_association_last_validation_check_time().has_value());
 }
 
-TEST_F(AddValidatedOriginAssociationsCommandTest, NotNeededEmpty) {
+TEST_F(AddValidatedOriginAssociationsCommandTest, EmptyReturnsSuccess) {
   GURL start_url("https://example.com/");
   // No scope extensions.
   webapps::AppId app_id = InstallApp(start_url, {});
+  clock().Advance(base::Days(1));
 
   base::HistogramTester tester;
   base::test::TestFuture<AddValidatedOriginAssociationsResult> future;
   provider().scheduler().ScheduleAddValidatedOriginAssociations(
       app_id, future.GetCallback());
-  ASSERT_EQ(AddValidatedOriginAssociationsResult::kNotNeeded, future.Get());
+
+  ASSERT_EQ(AddValidatedOriginAssociationsResult::kSuccess, future.Get());
   tester.ExpectUniqueSample("WebApp.AddValidatedOriginAssociations",
-                            AddValidatedOriginAssociationsResult::kNotNeeded,
-                            1);
+                            AddValidatedOriginAssociationsResult::kSuccess, 1);
 }
 
-TEST_F(AddValidatedOriginAssociationsCommandTest, NotNeededValidated) {
+TEST_F(AddValidatedOriginAssociationsCommandTest,
+       ValidateTwoTimesStillSuccess) {
   fake_origin_association_manager()->set_pass_through(true);
 
   GURL start_url("https://example.com/");
@@ -354,10 +356,9 @@
 
   provider().scheduler().ScheduleAddValidatedOriginAssociations(
       app_id, future.GetCallback());
-  ASSERT_EQ(AddValidatedOriginAssociationsResult::kNotNeeded, future.Get());
+  ASSERT_EQ(AddValidatedOriginAssociationsResult::kSuccess, future.Get());
   tester.ExpectUniqueSample("WebApp.AddValidatedOriginAssociations",
-                            AddValidatedOriginAssociationsResult::kNotNeeded,
-                            1);
+                            AddValidatedOriginAssociationsResult::kSuccess, 1);
 }
 
 TEST_F(AddValidatedOriginAssociationsCommandTest, AppDisabled) {
@@ -410,16 +411,125 @@
   EXPECT_FALSE(app->validated_migration_sources().empty());
   EXPECT_EQ(migration_source, *app->validated_migration_sources().begin());
 
-  // Check that is not needed.
+  // Still success on repeatable validation.
   base::HistogramTester tester2;
   clock().Advance(base::Days(1) + base::Seconds(1));
   base::test::TestFuture<AddValidatedOriginAssociationsResult> future2;
   provider().scheduler().ScheduleAddValidatedOriginAssociations(
       app_id, future2.GetCallback());
-  EXPECT_EQ(AddValidatedOriginAssociationsResult::kNotNeeded, future2.Get());
+  EXPECT_EQ(AddValidatedOriginAssociationsResult::kSuccess, future2.Get());
   tester2.ExpectUniqueSample("WebApp.AddValidatedOriginAssociations",
-                             AddValidatedOriginAssociationsResult::kNotNeeded,
-                             1);
+                             AddValidatedOriginAssociationsResult::kSuccess, 1);
+}
+
+TEST_F(AddValidatedOriginAssociationsCommandTest, RemoveStaleScopeExtension) {
+  GURL start_url("https://example.com/");
+  ScopeExtensionInfo extension1 = ScopeExtensionInfo::CreateForScope(
+      GURL("https://example.org/scope"), /*has_origin_wildcard=*/false);
+  ScopeExtensionInfo extension2 = ScopeExtensionInfo::CreateForScope(
+      GURL("https://example.com/scope"), /*has_origin_wildcard=*/false);
+
+  // Install will not validate scope extensions.
+  fake_origin_association_manager()->set_pass_through(false);
+  webapps::AppId app_id = InstallApp(start_url, {extension1, extension2});
+  clock().Advance(base::Days(1));
+
+  // First validation: both extensions are valid.
+  fake_origin_association_manager()->SetData(
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/web_applications/commands/add_validated_origin_associations_command_unittest.cc b/chrome/browser/web_applications/commands/add_validated_origin_associations_command_unittest.cc
index 2e9d7b6..5debe34 100644
--- a/chrome/browser/web_applications/commands/add_validated_origin_associations_command_unittest.cc
+++ b/chrome/browser/web_applications/commands/add_validated_origin_associations_command_unittest.cc
@@ -318,22 +318,24 @@
   EXPECT_TRUE(app->origin_association_last_validation_check_time().has_value());
 }
 
-TEST_F(AddValidatedOriginAssociationsCommandTest, NotNeededEmpty) {
+TEST_F(AddValidatedOriginAssociationsCommandTest, EmptyReturnsSuccess) {
   GURL start_url("https://example.com/");
   // No scope extensions.
   webapps::AppId app_id = InstallApp(start_url, {});
+  clock().Advance(base::Days(1));
 
   base::HistogramTester tester;
   base::test::TestFuture<AddValidatedOriginAssociationsResult> future;
   provider().scheduler().ScheduleAddValidatedOriginAssociations(
       app_id, future.GetCallback());
-  ASSERT_EQ(AddValidatedOriginAssociationsResult::kNotNeeded, future.Get());
+
+  ASSERT_EQ(AddValidatedOriginAssociationsResult::kSuccess, future.Get());
   tester.ExpectUniqueSample("WebApp.AddValidatedOriginAssociations",
-                            AddValidatedOriginAssociationsResult::kNotNeeded,
-                            1);
+                            AddValidatedOriginAssociationsResult::kSuccess, 1);
 }
 
-TEST_F(AddValidatedOriginAssociationsCommandTest, NotNeededValidated) {
+TEST_F(AddValidatedOriginAssociationsCommandTest,
+       ValidateTwoTimesStillSuccess) {
   fake_origin_association_manager()->set_pass_through(true);
 
   GURL start_url("https://example.com/");
@@ -354,10 +356,9 @@
 
   provider().scheduler().ScheduleAddValidatedOriginAssociations(
       app_id, future.GetCallback());
-  ASSERT_EQ(AddValidatedOriginAssociationsResult::kNotNeeded, future.Get());
+  ASSERT_EQ(AddValidatedOriginAssociationsResult::kSuccess, future.Get());
   tester.ExpectUniqueSample("WebApp.AddValidatedOriginAssociations",
-                            AddValidatedOriginAssociationsResult::kNotNeeded,
-                            1);
+                            AddValidatedOriginAssociationsResult::kSuccess, 1);
 }
 
 TEST_F(AddValidatedOriginAssociationsCommandTest, AppDisabled) {
@@ -410,16 +411,125 @@
   EXPECT_FALSE(app->validated_migration_sources().empty());
   EXPECT_EQ(migration_source, *app->validated_migration_sources().begin());
 
-  // Check that is not needed.
+  // Still success on repeatable validation.
   base::HistogramTester tester2;
   clock().Advance(base::Days(1) + base::Seconds(1));
   base::test::TestFuture<AddValidatedOriginAssociationsResult> future2;
   provider().scheduler().ScheduleAddValidatedOriginAssociations(
       app_id, future2.GetCallback());
-  EXPECT_EQ(AddValidatedOriginAssociationsResult::kNotNeeded, future2.Get());
+  EXPECT_EQ(AddValidatedOriginAssociationsResult::kSuccess, future2.Get());
   tester2.ExpectUniqueSample("WebApp.AddValidatedOriginAssociations",
-                             AddValidatedOriginAssociationsResult::kNotNeeded,
-                             1);
+                             AddValidatedOriginAssociationsResult::kSuccess, 1);
+}
+
+TEST_F(AddValidatedOriginAssociationsCommandTest, RemoveStaleScopeExtension) {
+  GURL start_url("https://example.com/");
+  ScopeExtensionInfo extension1 = ScopeExtensionInfo::CreateForScope(
+      GURL("https://example.org/scope"), /*has_origin_wildcard=*/false);
+  ScopeExtensionInfo extension2 = ScopeExtensionInfo::CreateForScope(
+      GURL("https://example.com/scope"), /*has_origin_wildcard=*/false);
+
+  // Install will not validate scope extensions.
+  fake_origin_association_manager()->set_pass_through(false);
+  webapps::AppId app_id = InstallApp(start_url, {extension1, extension2});
+  clock().Advance(base::Days(1));
+
+  // First validation: both extensions are valid.
+  fake_origin_association_manager()->SetData(
+      {{extension1, extension1}, {extension2, extension2}});
+
+  {
+    base::test::TestFuture<AddValidatedOriginAssociationsResult> future;
+    provider().scheduler().ScheduleAddValidatedOriginAssociations(
+        app_id, future.GetCallback());
+    ASSERT_EQ(AddValidatedOriginAssociationsResult::kSuccess, future.Get());
+
+    const WebApp* app = provider().registrar_unsafe().GetAppById(app_id);
+    EXPECT_EQ(app->validated_scope_extensions().size(), 2u);
+  }
+
+  // Advance clock to bypass throttling.
+  clock().Advance(base::Days(1) + base::Seconds(1));
+
+  // Second validation: extension1 is no longer valid (removed from association
+  // file).
+  fake_origin_association_manager()->SetData({{extension2, extension2}});
+
+  {
+    base::test::TestFuture<AddValidatedOriginAssociationsResult> future;
+    provider().scheduler().ScheduleAddValidatedOriginAssociations(
+        app_id, future.GetCallback());
+    ASSERT_EQ(AddValidatedOriginAssociationsResult::kUnvalidatedItemsRemain,
+              future.Get());
+
+    const WebApp* app = provider().registrar_unsafe().GetAppById(app_id);
+    EXPECT_EQ(app->validated_scope_extensions().size(), 1u);
+    EXPECT_EQ(extension2, *app->validated_scope_extensions().begin());
+  }
+}
+
+TEST_F(AddValidatedOriginAssociationsCommandTest, RemoveStaleMigrationSource) {
+  base::test::ScopedFeatureList scoped_feature_list;
+  scoped_feature_list.InitAndEnableFeature(
+      blink::features::kWebAppMigrationApi);
+
+  GURL start_url("https://example.com/");
+  auto info = WebAppInstallInfo::CreateWithStartUrlForTesting(start_url);
+  info->title = u"Test App";
+  webapps::AppId app_id = test::InstallWebApp(profile(), std::move(info));
+
+  MigrationSource migration_source1(
+      webapps::ManifestId(GURL("https://example.org/manifest.json")),
+      MigrationBehavior::kForce, GURL("https://example.org/subpath"));
+  MigrationSource migration_source2(
+      webapps::ManifestId(GURL("https://example.com/manifest.json")),
+      MigrationBehavior::kForce, GURL("https://example.com/subpath"));
+
+  {
+    ScopedRegistryUpdate update = provider().sync_bridge_unsafe().BeginUpdate();
+    WebApp* app_to_update = update->UpdateApp(app_id);
+    app_to_update->SetUnvalidatedMigrationSources(
+        {migration_source1, migration_source2});
+  }
+
+  fake_origin_association_manager()->set_pass_through(false);
+  clock().Advance(base::Days(1) + base::Seconds(1));
+
+  // First validation: both migration sources are valid.
+  fake_origin_association_manager()->SetMigrationSourcesData(
+      {migration_source1.manifest_id(), migration_source2.manifest_id()});
+
+  EXPECT_CALL(mock_scheduler(), ScheduleResolveWebAppPendingMigrationInfo(_, _))
+      .Times(2);
+
+  {
+    base::test::TestFuture<AddValidatedOriginAssociationsResult> future;
+    provider().scheduler().ScheduleAddValidatedOriginAssociations(
+        app_id, future.GetCallback());
+    ASSERT_EQ(AddValidatedOriginAssociationsResult::kSuccess, future.Get());
+
+    const WebApp* app = provider().registrar_unsafe().GetAppById(app_id);
+    EXPECT_EQ(app->validated_migration_sources().size(), 2u);
+  }
+
+  // Advance clock to bypass throttling.
+  clock().Advance(base::Days(1) + base::Seconds(1));
+
+  // Second validation: migration_source1 is no longer valid.
+  fake_origin_association_manager()->SetMigrationSourcesData(
+      {migration_source2.manifest_id()});
+
+  {
+    base::test::TestFuture<AddValidatedOriginAssociationsResult> future;
+    provider().scheduler().ScheduleAddValidatedOriginAssociations(
+        app_id, future.GetCallback());
+    ASSERT_EQ(AddValidatedOriginAssociationsResult::kUnvalidatedItemsRemain,
+              future.Get());
+
+    const WebApp* app = provider().registrar_unsafe().GetAppById(app_id);
+    EXPECT_EQ(app->validated_migration_sources().size(), 1u);
+    EXPECT_EQ(migration_source2, *app->validated_migration_sources().begin());
+  }
 }
 
 }  // namespace web_app
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.