CVE-2026-8004
Overview
Background
- DevTools `Storage` domain
- A Chrome DevTools Protocol (CDP) domain whose handler (
StorageHandler) exposes storage introspection commands to any attached DevTools client, including extensions using thechrome.debuggerAPI. - Protected Audience / Interest Groups (FLEDGE)
- The privacy-sandbox on-device ad-auction API; interest groups contain bidding logic URLs, ads, joining origins, and auction activity that reveal browsing behavior.
- `MayAttachToURL()`
- The DevTools access-control check that enforces (among other things) enterprise policies restricting which URLs/origins a debugger client may inspect.
- `AddInterestGroupObserver`
- Registers an observer on the profile-wide
InterestGroupManagerImpl, receiving interest-group access events across all tabs in the profile.
Root Cause Analysis
StorageHandler::GetInterestGroupDetails looked up and serialized an interest group by owner origin and returned its full details (ads, bidding logic URL, joining origin) to the DevTools client without ever consulting MayAttachToURL(), so a client blocked by enterprise policy from a domain could still read that domain’s sensitive bidding data and, via joining origins, infer browsing history. Separately, StorageHandler::SetInterestGroupTracking (through SetInterestGroupTrackingInternal) registered the handler as a profile-wide observer on InterestGroupManagerImpl, so Protected Audience activity from every tab in the profile leaked to the attached client rather than being scoped to what that client is permitted to see. The violated invariant is that DevTools data exposure must honor the client’s attach-to-URL/policy restrictions and tab scope. Because the Protected Audience API is deprecated, the fix neutralizes both commands rather than adding scoped checks: GetInterestGroupDetails now immediately sendSuccess with an empty DictValue, SetInterestGroupTracking returns Response::Success() without registering any observer, and SetInterestGroupTrackingInternal becomes a no-op, so no interest-group data or cross-tab activity is emitted at all.GetInterestGroupDetails returned sensitive interest-group data with no MayAttachToURL() policy check, and SetInterestGroupTracking installed a profile-wide observer leaking cross-tab activity; the fix stubs all three methods to return empty/success so neither the data nor the events are exposed.Attack Path
- Attach a DevTools client A Chrome extension (or other CDP client) attaches to a target via the debugger protocol, subject to enterprise policy that should block certain domains.
- Enable tracking
The client calls
Storage.setInterestGroupTracking(true); pre-patch this registers a profile-wide observer, deliveringinterestGroupAccessedevents for Protected Audience activity across all tabs. - Request details
The client calls
Storage.getInterestGroupDetailsfor a target owner origin; pre-patch the handler returns the group’s ads, bidding logic URL, and joining origin with noMayAttachToURL()check. - Policy-bypassing disclosure The client thereby reads sensitive bidding data and browsing-history signals (joining origins) for domains it was explicitly blocked from, and observes profile-wide auction activity it should not see.
Impact Assessment
MayAttachToURL() and the tracking command registered a profile-wide observer. The precondition is an attached DevTools/debugger client (e.g. an installed extension with debugging capability). The vendor advisory rates this low severity and classifies it as insufficient policy enforcement in DevTools; no exploitation-in-the-wild is stated.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/devtools/protocol/storage_handler.cc |
modified |
Files Changed
content/browser/devtools/protocol/storage_handler.ccthird_party/blink/web_tests/http/tests/inspector-protocol/storage/interest-groups-expected.txt
Audit Directions
- CDP handlers missing attach checksAudit
StorageHandlerand other DevTools domain handlers for data-returning commands that omitMayAttachToURL()/policy checks reachable by extension-based clients. - Profile-wide observers from per-client commandsFlag commands that register observers on profile-scoped managers (like
InterestGroupManagerImpl), which can leak activity beyond the client’s permitted tab/origin scope. - Deprecated-API surfaceReview CDP commands backing deprecated features, since they often retain unmaintained data-exposure paths that bypass newer access controls.
Patch
From bf745058e2e8a43f342c49d5b985d793f0818bfb Mon Sep 17 00:00:00 2001
From: Danil Somsikov <dsv@chromium.org>
Date: Thu, 02 Apr 2026 05:38:20 -0700
Subject: [PATCH] Disable InterestGroup tracking and details in DevTools
The Protected Audience API is deprecated and the current implementation has potential security issues: `Storage.setInterestGroupTracking` registered a profile-wide observer, leaking Protected Audience activity across all tabs to any
DevTools client. Additionally, `Storage.getInterestGroupDetails` did not
check `MayAttachToURL()`, allowing a DevTools client (such as a Chrome
extension) to retrieve sensitive bidding data and browsing history from
domains it was explicitly blocked from accessing via enterprise
policies.
To address this issue:
- `StorageHandler::SetInterestGroupTracking` now returns success without
registering the `InterestGroupManager` observer.
- `StorageHandler::GetInterestGroupDetails` now immediately returns an
empty response, ensuring no sensitive data is leaked.
- Test expectations have been updated to reflect that
`interestGroupAccessed` events and `interestGroupDetails` are no
longer recorded or returned.
Fixed: 496189510
Change-Id: I32185011cb06cf7b1e89622f068151be71cc6252
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7715378
Commit-Queue: Danil Somsikov <dsv@chromium.org>
Auto-Submit: Danil Somsikov <dsv@chromium.org>
Reviewed-by: Andrey Kosyakov <caseq@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1609132}
---
diff --git a/content/browser/devtools/protocol/storage_handler.cc b/content/browser/devtools/protocol/storage_handler.cc
index e240043..1c3b3bd 100644
--- a/content/browser/devtools/protocol/storage_handler.cc
+++ b/content/browser/devtools/protocol/storage_handler.cc
@@ -1112,87 +1112,26 @@
bid, bid_currency.CopyAsOptional(), auction_id.CopyAsOptional());
}
-namespace {
-void SendGetInterestGroup(
- std::unique_ptr<StorageHandler::GetInterestGroupDetailsCallback> callback,
- std::optional<SingleStorageInterestGroup> storage_group) {
- if (!storage_group) {
- callback->sendFailure(Response::ServerError("Interest group not found"));
- return;
- }
-
- base::DictValue ig_serialization =
- SerializeInterestGroupForDevtools(storage_group.value()->interest_group);
-
- // "joiningOrigin" is in StorageInterestGroup, not InterestGroup, so it needs
- // to be added in separately.
- ig_serialization.Set("joiningOrigin",
- storage_group.value()->joining_origin.Serialize());
- callback->sendSuccess(
- std::make_unique<base::DictValue>(std::move(ig_serialization)));
-}
-
-} // namespace
-
void StorageHandler::GetInterestGroupDetails(
const std::string& owner_origin_string,
const std::string& name,
std::unique_ptr<GetInterestGroupDetailsCallback> callback) {
- if (!storage_partition_) {
- callback->sendFailure(Response::InternalError());
- return;
- }
-
- InterestGroupManagerImpl* manager = static_cast<InterestGroupManagerImpl*>(
- storage_partition_->GetInterestGroupManager());
- if (!manager) {
- callback->sendFailure(
- Response::ServerError("Interest group storage is disabled"));
- return;
- }
-
- GURL owner_origin_url(owner_origin_string);
- if (!owner_origin_url.is_valid()) {
- callback->sendFailure(Response::ServerError("Invalid Owner Origin"));
- return;
- }
- url::Origin owner_origin = url::Origin::Create(GURL(owner_origin_string));
- DCHECK(!owner_origin.opaque());
-
- manager->GetInterestGroup(
- owner_origin, name,
- base::BindOnce(&SendGetInterestGroup, std::move(callback)));
+ // TODO(crbug.com/496189510): Remove this completely once the DevTools
+ // frontend usage is gone.
+ callback->sendSuccess(std::make_unique<base::DictValue>());
}
Response StorageHandler::SetInterestGroupTracking(bool enable) {
- interest_group_tracking_enabled_ = enable;
- return SetInterestGroupTrackingInternal(storage_partition_, enable);
+ // TODO(crbug.com/496189510): Remove this completely once the DevTools
+ // frontend usage is gone.
+ return Response::Success();
}
Response StorageHandler::SetInterestGroupTrackingInternal(
StoragePartition* storage_partition,
bool enable) {
- if (!storage_partition) {
- return Response::InternalError();
- }
-
- InterestGroupManagerImpl* manager = static_cast<InterestGroupManagerImpl*>(
- storage_partition->GetInterestGroupManager());
- if (!manager) {
- return Response::ServerError("Interest group storage is disabled.");
- }
-
- if (enable) {
- // Only add if we are not already registered as an observer. We only
- // observe the interest group manager, so if we're observing anything then
- // we are already registered.
- if (!InterestGroupManagerImpl::InterestGroupObserver::IsInObserverList()) {
- manager->AddInterestGroupObserver(this);
- }
- } else {
- // Removal doesn't care if we are not registered.
- manager->RemoveInterestGroupObserver(this);
- }
+ // TODO(crbug.com/496189510): Remove this completely once the DevTools
+ // frontend usage is gone.
return Response::Success();
}
diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/storage/interest-groups-expected.txt b/third_party/blink/web_tests/http/tests/inspector-protocol/storage/interest-groups-expected.txt
index 5efa96a..aace1b8 100644
--- a/third_party/blink/web_tests/http/tests/inspector-protocol/storage/interest-groups-expected.txt
+++ b/third_party/blink/web_tests/http/tests/inspector-protocol/storage/interest-groups-expected.txt
@@ -45,162 +45,6 @@
type : sellerJs
url : https://a.test:8443/inspector-protocol/resources/fledge_decision_logic.js.php
}
-interestGroupAccessed {
- accessTime : <number>
- name : 0
- ownerOrigin : https://a.test:8443
- type : join
- uniqueAuctionId : global
-}
-interestGroupDetails {
- ads : [
- [0] : {
- metadata : {"ad":"metadata","here":[1,2,3]}
- renderURL : https://example.com/render0
- }
- ]
- biddingLogicURL : https://a.test:8443/inspector-protocol/resources/fledge_bidding_logic.js.php
- executionMode : compatibility
- joiningOrigin : https://a.test:8443
- name : 0
- ownerOrigin : https://a.test:8443
- priority : 0
- trustedBiddingSignalsURL : https://a.test:8443/inspector-protocol/resources/fledge_bidding_signals.js.php
-}
-interestGroupAccessed {
- accessTime : <number>
- name : 1
- ownerOrigin : https://a.test:8443
- type : join
- uniqueAuctionId : global
-}
-interestGroupDetails {
- ads : [
- [0] : {
- metadata : {"ad":"metadata","here":[1,2,3]}
- renderURL : https://example.com/render1
- }
- ]
- biddingLogicURL : https://a.test:8443/inspector-protocol/resources/fledge_bidding_logic.js.php
- executionMode : compatibility
- joiningOrigin : https://a.test:8443
- name : 1
- ownerOrigin : https://a.test:8443
- priority : 0
- trustedBiddingSignalsURL : https://a.test:8443/inspector-protocol/resources/fledge_bidding_signals.js.php
-}
-interestGroupAccessed {
- accessTime : <number>
- name : 0
- ownerOrigin : https://a.test:8443
- type : loaded
- uniqueAuctionId : 1
-}
-interestGroupDetails {
- ads : [
- [0] : {
- metadata : {"ad":"metadata","here":[1,2,3]}
- renderURL : https://example.com/render0
- }
- ]
- biddingLogicURL : https://a.test:8443/inspector-protocol/resources/fledge_bidding_logic.js.php
- executionMode : compatibility
- joiningOrigin : https://a.test:8443
- name : 0
Regression Test / PoC
diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/storage/interest-groups-expected.txt b/third_party/blink/web_tests/http/tests/inspector-protocol/storage/interest-groups-expected.txt
index 5efa96a..aace1b8 100644
--- a/third_party/blink/web_tests/http/tests/inspector-protocol/storage/interest-groups-expected.txt
+++ b/third_party/blink/web_tests/http/tests/inspector-protocol/storage/interest-groups-expected.txt
@@ -45,162 +45,6 @@
type : sellerJs
url : https://a.test:8443/inspector-protocol/resources/fledge_decision_logic.js.php
}
-interestGroupAccessed {
- accessTime : <number>
- name : 0
- ownerOrigin : https://a.test:8443
- type : join
- uniqueAuctionId : global
-}
-interestGroupDetails {
- ads : [
- [0] : {
- metadata : {"ad":"metadata","here":[1,2,3]}
- renderURL : https://example.com/render0
- }
- ]
- biddingLogicURL : https://a.test:8443/inspector-protocol/resources/fledge_bidding_logic.js.php
- executionMode : compatibility
- joiningOrigin : https://a.test:8443
- name : 0
- ownerOrigin : https://a.test:8443
- priority : 0
- trustedBiddingSignalsURL : https://a.test:8443/inspector-protocol/resources/fledge_bidding_signals.js.php
-}
-interestGroupAccessed {
- accessTime : <number>
- name : 1
- ownerOrigin : https://a.test:8443
- type : join
- uniqueAuctionId : global
-}
-interestGroupDetails {
- ads : [
- [0] : {
- metadata : {"ad":"metadata","here":[1,2,3]}
- renderURL : https://example.com/render1
- }
- ]
- biddingLogicURL : https://a.test:8443/inspector-protocol/resources/fledge_bidding_logic.js.php
- executionMode : compatibility
- joiningOrigin : https://a.test:8443
- name : 1
- ownerOrigin : https://a.test:8443
- priority : 0
- trustedBiddingSignalsURL : https://a.test:8443/inspector-protocol/resources/fledge_bidding_signals.js.php
-}
-interestGroupAccessed {
- accessTime : <number>
- name : 0
- ownerOrigin : https://a.test:8443
- type : loaded
- uniqueAuctionId : 1
-}
-interestGroupDetails {
- ads : [
- [0] : {
- metadata : {"ad":"metadata","here":[1,2,3]}
- renderURL : https://example.com/render0
- }
- ]
- biddingLogicURL : https://a.test:8443/inspector-protocol/resources/fledge_bidding_logic.js.php
- executionMode : compatibility
- joiningOrigin : https://a.test:8443
- name : 0
- ownerOrigin : https://a.test:8443
- priority : 0
- trustedBiddingSignalsURL : https://a.test:8443/inspector-protocol/resources/fledge_bidding_signals.js.php
-}
-interestGroupAccessed {
- accessTime : <number>
- name : 1
- ownerOrigin : https://a.test:8443
- type : loaded
- uniqueAuctionId : 1
-}
-interestGroupDetails {
- ads : [
- [0] : {
- metadata : {"ad":"metadata","here":[1,2,3]}
- renderURL : https://example.com/render1
- }
- ]
- biddingLogicURL : https://a.test:8443/inspector-protocol/resources/fledge_bidding_logic.js.php
- executionMode : compatibility
- joiningOrigin : https://a.test:8443
- name : 1
- ownerOrigin : https://a.test:8443
- priority : 0
- trustedBiddingSignalsURL : https://a.test:8443/inspector-protocol/resources/fledge_bidding_signals.js.php
-}
-interestGroupAccessed {
- accessTime : <number>
- bid : 1
- name : 0
- ownerOrigin : https://a.test:8443
- type : bid
- uniqueAuctionId : 1
-}
-interestGroupDetails {
- ads : [
- [0] : {
- metadata : {"ad":"metadata","here":[1,2,3]}
- renderURL : https://example.com/render0
- }
- ]
- biddingLogicURL : https://a.test:8443/inspector-protocol/resources/fledge_bidding_logic.js.php
- executionMode : compatibility
- joiningOrigin : https://a.test:8443
- name : 0
- ownerOrigin : https://a.test:8443
- priority : 0
- trustedBiddingSignalsURL : https://a.test:8443/inspector-protocol/resources/fledge_bidding_signals.js.php
-}
-interestGroupAccessed {
- accessTime : <number>
- bid : 2
- name : 1
- ownerOrigin : https://a.test:8443
- type : bid
- uniqueAuctionId : 1
-}
-interestGroupDetails {
- ads : [
- [0] : {
- metadata : {"ad":"metadata","here":[1,2,3]}
- renderURL : https://example.com/render1
- }
- ]
- biddingLogicURL : https://a.test:8443/inspector-protocol/resources/fledge_bidding_logic.js.php
- executionMode : compatibility
- joiningOrigin : https://a.test:8443
- name : 1
- ownerOrigin : https://a.test:8443
- priority : 0
- trustedBiddingSignalsURL : https://a.test:8443/inspector-protocol/resources/fledge_bidding_signals.js.php
-}
-interestGroupAccessed {
- accessTime : <number>
- name : 1
- ownerOrigin : https://a.test:8443
- type : win
- uniqueAuctionId : 1
-}
-interestGroupDetails {
- ads : [
- [0] : {
- metadata : {"ad":"metadata","here":[1,2,3]}
- renderURL : https://example.com/render1
- }
- ]
- biddingLogicURL : https://a.test:8443/inspector-protocol/resources/fledge_bidding_logic.js.php
- executionMode : compatibility
- joiningOrigin : https://a.test:8443
- name : 1
- ownerOrigin : https://a.test:8443
- priority : 0
- trustedBiddingSignalsURL : https://a.test:8443/inspector-protocol/resources/fledge_bidding_signals.js.php
-}
Stop Tracking IG Events
Logged IG events:
interestGroupAuctionEventOccurred {
Original Bug Report
Potential cross-tab tracking and policy bypass via Storage.setInterestGroupTracking
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: The Storage.setInterestGroupTracking DevTools method registers a profile-wide observer that leaks Protected Audience activity across all tabs. Furthermore, Storage.getInterestGroupDetails fails to verify the target URL against enterprise policies using MayAttachToURL(). This allows a malicious extension to monitor sensitive bidding data and browsing history from restricted origins, bypassing tab isolation and enterprise policies.
Affected files:
content/browser/devtools/protocol/storage_handler.cccontent/browser/interest_group/interest_group_manager_impl.cccontent/browser/interest_group/interest_group_manager_impl.h
Estimated timestamp from git blame: 2024-11-25
Summary
There is a potential security vulnerability in the DevTools StorageHandler where a chrome.debugger extension attached to a single allowed tab can receive a real-time stream of Protected Audience (formerly FLEDGE) activity across the user’s entire browsing session. This bypasses enterprise runtime_blocked_hosts policies and tab isolation.
Technical Details
- Profile-Wide Observation: In
content/browser/devtools/protocol/storage_handler.cc, theSetInterestGroupTrackingInternalmethod registers theStorageHandleras an observer onInterestGroupManagerImpl. BecauseInterestGroupManagerImplis scoped to theStoragePartition(the entire profile), the observer listens for all interest group events globally. - Unfiltered Broadcasting: When an interest group event occurs in any tab,
InterestGroupManagerImpl::NotifyInterestGroupAccessediterates through all observers unconditionally. It does not filter byRenderFrameHostor tab ID. TheStorageHandler::OnInterestGroupAccessedthen forwards the event details (includingowner_originandname) to the DevTools frontend. - Missing Authorization Check: The
StorageHandler::GetInterestGroupDetailsmethod retrieves the full interest group configuration using an(owner_origin, name)pair. Unlike other sensitive methods inStorageHandler(e.g.,GetCookies, which filters results throughNetworkHandler::CanAccessCookieandclient_->MayAttachToURL()),GetInterestGroupDetailsentirely omits theMayAttachToURL()check.
Because ExtensionDevToolsClientHost overrides MayAttachToURL to enforce the runtime_blocked_hosts enterprise policy, this omission allows an extension to retrieve sensitive ad-tech user segmentation data (userBiddingSignals) and the exact URL where the user joined the group (joiningOrigin, effectively leaking browsing history) from domains it is explicitly blocked from accessing.
Potential Reproduction Steps
Note: These are suggested steps; our setup does not have the ability to execute code to verify them.
- An attacker creates a Chrome extension with the
debuggerpermission. - The victim installs the extension in an environment with enterprise policies blocking access to
restricted-domain.com. - The extension attaches its debugger to a benign, permitted tab:
chrome.debugger.attach({tabId: allowedTabId}, '1.3'). - The extension enables interest group tracking:
chrome.debugger.sendCommand({tabId: allowedTabId}, 'Storage.setInterestGroupTracking', {enable: true}). - The extension listens for
chrome.debugger.onEventwith the methodStorage.interestGroupAccessed. - The victim opens a new tab and navigates to
restricted-domain.com, which callsnavigator.joinAdInterestGroup(). - The extension receives the event containing the
ownerOriginandnamefrom the restricted tab. - The extension uses the observed
ownerOriginandnameto callStorage.getInterestGroupDetails, successfully retrieving the full interest group data, includingjoiningOriginanduserBiddingSignals, bypassing the enterprise blocklist.
Suggested Fix
- Add Permission Checks: In
StorageHandler::GetInterestGroupDetails, verify the requestedowner_originagainstclient_->MayAttachToURL()before querying the database, similar to how cookies are handled. - Filter Observer Events:
InterestGroupManagerImplorStorageHandlershould filter interest group access events. Events should only be dispatched to DevTools clients that are attached to theRenderFrameHostorWebContentswhere the event originated, or at a minimum, theowner_originof the event should be checked againstMayAttachToURL()before being forwarded inStorageHandler::OnInterestGroupAccessed.
Evaluated with Chrome root at commit: bb48272cafb7e24c93f55ef40da398cd206ee651
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. Please feel free to reach out to me if you have concerns or feedback.