Chrome · Isolated Web Apps
CVE-2026-14042
Logic Error in Isolated Web Apps
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/web_applications/commands/add_validated_origin_associations_command.cc |
modified | |
AddValidatedOriginAssociationsCommandchrome/browser/web_applications/commands/add_validated_origin_associations_command.h |
modified | |
TEST_Fchrome/browser/web_applications/commands/add_validated_origin_associations_command_unittest.cc |
modified |
Files Changed
chrome/browser/web_applications/commands/add_validated_origin_associations_command.ccchrome/browser/web_applications/commands/add_validated_origin_associations_command.hchrome/browser/web_applications/commands/add_validated_origin_associations_command_unittest.cc
Patch
From 0cae24a265c166f3d69538fd92ab89d171303778 Mon Sep 17 00:00:00 2001
From: Vlad Krot <vkrot@google.com>
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 <vkrot@google.com>
Reviewed-by: Dibyajyoti Pal <dibyapal@chromium.org>
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.
References
On This Page