Chrome · Extensions
CVE-2026-14053
Logic Error in Extensions
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifextensions/browser/api/storage/storage_frontend.cc |
modified | |
EventRestrictionObserverextensions/browser/api/storage/storage_frontend_unittest.cc |
modified | |
TEST_Fextensions/browser/api/storage/storage_frontend_unittest.cc |
modified |
Files Changed
extensions/browser/api/storage/storage_frontend.ccextensions/browser/api/storage/storage_frontend_unittest.cc
Patch
From 19a50035bed117f2538aa31e647e69f216011e35 Mon Sep 17 00:00:00 2001 From: Eva Su <[email protected]> Date: Fri, 15 May 2026 15:37:55 -0700 Subject: [PATCH] [Extensions] Respect storage access levels for onChanged events This CL ensures that storage access restrictions are correctly applied to onChanged events even when the triggering source does not provide an explicit access level. This occurs for updates originating from Chrome Sync or enterprise policy. Previously, the frontend would default to unrestricted access (delivering the event to all contexts, including untrusted content scripts) if the access level was omitted. With this change, the frontend now retrieves the extension's configured access level for that specific storage area if none is provided. This ensures that the default TRUSTED_CONTEXTS restriction for areas like session storage cannot be bypassed if an event is ever triggered without an explicit access level. Fixed: 501836539 Change-Id: Ic00848cf47269d133887d3299e5c92eaa12331de Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7851231 Reviewed-by: Andrea Orru <[email protected]> Commit-Queue: Eva Su <[email protected]> Cr-Commit-Position: refs/heads/main@{#1631608} --- diff --git a/extensions/browser/api/storage/storage_frontend.cc b/extensions/browser/api/storage/storage_frontend.cc index d816e03..5c70ae28 100644 --- a/extensions/browser/api/storage/storage_frontend.cc +++ b/extensions/browser/api/storage/storage_frontend.cc @@ -670,11 +670,20 @@ bool has_area_changed_event_listener = event_router->ExtensionHasEventListener(extension_id, area_event_name); + if (!has_event_changed_listener && !has_area_changed_event_listener) { + return; + } + + api::storage::AccessLevel effective_access_level = + access_level.has_value() + ? access_level.value() + : storage_utils::GetAccessLevelForArea( + extension_id, *browser_context_, storage_area); + + std::optional<mojom::ContextType> restrict_to_context_type = std::nullopt; // Restrict event to privileged context if access level is set only to trusted // contexts. - std::optional<mojom::ContextType> restrict_to_context_type = std::nullopt; - if (access_level.has_value() && - access_level.value() == api::storage::AccessLevel::kTrustedContexts) { + if (effective_access_level == api::storage::AccessLevel::kTrustedContexts) { restrict_to_context_type = mojom::ContextType::kPrivilegedExtension; } diff --git a/extensions/browser/api/storage/storage_frontend_unittest.cc b/extensions/browser/api/storage/storage_frontend_unittest.cc index 83ec1afe..0f1cf4b 100644 --- a/extensions/browser/api/storage/storage_frontend_unittest.cc +++ b/extensions/browser/api/storage/storage_frontend_unittest.cc @@ -22,7 +22,10 @@ #include "extensions/browser/api/storage/settings_test_util.h" #include "extensions/browser/api/storage/storage_area_namespace.h" #include "extensions/browser/api/storage/storage_frontend.h" +#include "extensions/browser/event_listener_map.h" +#include "extensions/browser/event_router.h" #include "extensions/browser/extensions_test.h" +#include "extensions/common/mojom/context_type.mojom.h" #include "testing/gtest/include/gtest/gtest.h" using value_store::ValueStore; @@ -87,6 +90,99 @@ EXPECT_FALSE(frontend_->GetValueStoreCache(settings::INVALID)); } +namespace { + +class EventRestrictionObserver : public EventRouter::TestObserver { + public: + struct DispatchedEvent { + std::string name; + std::optional<mojom::ContextType> restrict_to_context_type; + }; + + void OnWillDispatchEvent(const Event& event) override { + dispatched_events_.push_back( + {event.event_name, event.restrict_to_context_type}); + } + + void OnDidDispatchEventToProcess(const Event& event, + int process_id) override {} + + const std::vector<DispatchedEvent>& events() const { + return dispatched_events_; + } + + void Clear() { dispatched_events_.clear(); } + + private: + std::vector<DispatchedEvent> dispatched_events_; +}; + +} // namespace + +TEST_F(ExtensionSettingsFrontendTest, OnSettingsChanged_RestrictToContextType) { + EventRestrictionObserver observer; + EventRouter* event_router = EventRouter::Get(browser_context()); + event_router->AddObserverForTesting(&observer); + + const std::string id = "ext"; + scoped_refptr<const Extension> extension = + settings_test_util::AddExtensionWithId(browser_context(), id, + Manifest::Type::kExtension); + + event_router->listeners().AddListener(EventListener::CreateLazyListener( + "storage.session.onChanged", id, browser_context(), false, GURL(), + std::nullopt)); + event_router->listeners().AddListener(EventListener::CreateLazyListener( + "storage.sync.onChanged", id, browser_context(), false, GURL(), + std::nullopt)); + event_router->listeners().AddListener(EventListener::CreateLazyListener( + "storage.managed.onChanged", id, browser_context(), false, GURL(), + std::nullopt)); + event_router->listeners().AddListener(EventListener::CreateLazyListener( + "storage.onChanged", id, browser_context(), false, GURL(), std::nullopt)); + + SettingsChangedCallback callback = frontend_->GetObserver(); + + // Test with session storage. Default should be restricted to privileged + // contexts. + callback.Run(id, StorageAreaNamespace::kSession, std::nullopt, + base::Value(true)); + + ASSERT_EQ(observer.events().size(), 2u); + EXPECT_EQ(observer.events()[0].name, "storage.session.onChanged"); + EXPECT_EQ(observer.events()[0].restrict_to_context_type, + mojom::ContextType::kPrivilegedExtension); + EXPECT_EQ(observer.events()[1].name, "storage.onChanged"); + EXPECT_EQ(observer.events()[1].restrict_to_context_type, + mojom::ContextType::kPrivilegedExtension); + + observer.Clear(); + + // Test with sync storage. Default should NOT be restricted. + callback.Run(id, StorageAreaNamespace::kSync, std::nullopt, + base::Value(true)); + + ASSERT_EQ(observer.events().size(), 2u); + EXPECT_EQ(observer.events()[0].name, "storage.sync.onChanged"); + EXPECT_EQ(observer.events()[0].restrict_to_context_type, std::nullopt); + EXPECT_EQ(observer.events()[1].name, "storage.onChanged"); + EXPECT_EQ(observer.events()[1].restrict_to_context_type, std::nullopt); + + observer.Clear(); + + // Test with managed storage. Default should NOT be restricted. + callback.Run(id, StorageAreaNamespace::kManaged, std::nullopt, + base::Value(true)); + + ASSERT_EQ(observer.events().size(), 2u); + EXPECT_EQ(observer.events()[0].name, "storage.managed.onChanged"); + EXPECT_EQ(observer.events()[0].restrict_to_context_type, std::nullopt); + EXPECT_EQ(observer.events()[1].name, "storage.onChanged"); + EXPECT_EQ(observer.events()[1].restrict_to_context_type, std::nullopt); + + event_router->RemoveObserverForTesting(&observer); +} + TEST_F(ExtensionSettingsFrontendTest, SettingsPreservedAcrossReconstruction) { const std::string id = "ext"; scoped_refptr<const Extension> extension =
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/extensions/browser/api/storage/storage_frontend_unittest.cc b/extensions/browser/api/storage/storage_frontend_unittest.cc
index 83ec1afe..0f1cf4b 100644
--- a/extensions/browser/api/storage/storage_frontend_unittest.cc
+++ b/extensions/browser/api/storage/storage_frontend_unittest.cc
@@ -22,7 +22,10 @@
#include "extensions/browser/api/storage/settings_test_util.h"
#include "extensions/browser/api/storage/storage_area_namespace.h"
#include "extensions/browser/api/storage/storage_frontend.h"
+#include "extensions/browser/event_listener_map.h"
+#include "extensions/browser/event_router.h"
#include "extensions/browser/extensions_test.h"
+#include "extensions/common/mojom/context_type.mojom.h"
#include "testing/gtest/include/gtest/gtest.h"
using value_store::ValueStore;
@@ -87,6 +90,99 @@
EXPECT_FALSE(frontend_->GetValueStoreCache(settings::INVALID));
}
+namespace {
+
+class EventRestrictionObserver : public EventRouter::TestObserver {
+ public:
+ struct DispatchedEvent {
+ std::string name;
+ std::optional<mojom::ContextType> restrict_to_context_type;
+ };
+
+ void OnWillDispatchEvent(const Event& event) override {
+ dispatched_events_.push_back(
+ {event.event_name, event.restrict_to_context_type});
+ }
+
+ void OnDidDispatchEventToProcess(const Event& event,
+ int process_id) override {}
+
+ const std::vector<DispatchedEvent>& events() const {
+ return dispatched_events_;
+ }
+
+ void Clear() { dispatched_events_.clear(); }
+
+ private:
+ std::vector<DispatchedEvent> dispatched_events_;
+};
+
+} // namespace
+
+TEST_F(ExtensionSettingsFrontendTest, OnSettingsChanged_RestrictToContextType) {
+ EventRestrictionObserver observer;
+ EventRouter* event_router = EventRouter::Get(browser_context());
+ event_router->AddObserverForTesting(&observer);
+
+ const std::string id = "ext";
+ scoped_refptr<const Extension> extension =
+ settings_test_util::AddExtensionWithId(browser_context(), id,
+ Manifest::Type::kExtension);
+
+ event_router->listeners().AddListener(EventListener::CreateLazyListener(
+ "storage.session.onChanged", id, browser_context(), false, GURL(),
+ std::nullopt));
+ event_router->listeners().AddListener(EventListener::CreateLazyListener(
+ "storage.sync.onChanged", id, browser_context(), false, GURL(),
+ std::nullopt));
+ event_router->listeners().AddListener(EventListener::CreateLazyListener(
+ "storage.managed.onChanged", id, browser_context(), false, GURL(),
+ std::nullopt));
+ event_router->listeners().AddListener(EventListener::CreateLazyListener(
+ "storage.onChanged", id, browser_context(), false, GURL(), std::nullopt));
+
+ SettingsChangedCallback callback = frontend_->GetObserver();
+
+ // Test with session storage. Default should be restricted to privileged
+ // contexts.
+ callback.Run(id, StorageAreaNamespace::kSession, std::nullopt,
+ base::Value(true));
+
+ ASSERT_EQ(observer.events().size(), 2u);
+ EXPECT_EQ(observer.events()[0].name, "storage.session.onChanged");
+ EXPECT_EQ(observer.events()[0].restrict_to_context_type,
+ mojom::ContextType::kPrivilegedExtension);
+ EXPECT_EQ(observer.events()[1].name, "storage.onChanged");
+ EXPECT_EQ(observer.events()[1].restrict_to_context_type,
+ mojom::ContextType::kPrivilegedExtension);
+
+ observer.Clear();
+
+ // Test with sync storage. Default should NOT be restricted.
+ callback.Run(id, StorageAreaNamespace::kSync, std::nullopt,
+ base::Value(true));
+
+ ASSERT_EQ(observer.events().size(), 2u);
+ EXPECT_EQ(observer.events()[0].name, "storage.sync.onChanged");
+ EXPECT_EQ(observer.events()[0].restrict_to_context_type, std::nullopt);
+ EXPECT_EQ(observer.events()[1].name, "storage.onChanged");
+ EXPECT_EQ(observer.events()[1].restrict_to_context_type, std::nullopt);
+
+ observer.Clear();
+
+ // Test with managed storage. Default should NOT be restricted.
+ callback.Run(id, StorageAreaNamespace::kManaged, std::nullopt,
+ base::Value(true));
+
+ ASSERT_EQ(observer.events().size(), 2u);
+ EXPECT_EQ(observer.events()[0].name, "storage.managed.onChanged");
+ EXPECT_EQ(observer.events()[0].restrict_to_context_type, std::nullopt);
+ EXPECT_EQ(observer.events()[1].name, "storage.onChanged");
+ EXPECT_EQ(observer.events()[1].restrict_to_context_type, std::nullopt);
+
+ event_router->RemoveObserverForTesting(&observer);
+}
+
TEST_F(ExtensionSettingsFrontendTest, SettingsPreservedAcrossReconstruction) {
const std::string id = "ext";
scoped_refptr<const Extension> extension =
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