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 <evasu@chromium.org>
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 <andreaorru@chromium.org>
Commit-Queue: Eva Su <evasu@chromium.org>
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