Low watch CVSS 4.3 chrome Policy Bypass 📄 Reporter bug report 🔧 Commit mapped

Overview

Low
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient policy enforcement in DevTools
DescriptionInsufficient policy enforcement in DevTools
ComponentDevTools
Bug ClassPolicy Bypass
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker496189510
Fix commitbf745058e2e8 (chromium/src) +8/-225
CWECWE-693
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-03-25 (per vendor advisory)
Fix landed2026-04-02 (public git)
Disclosed2026-05-05
Patch gap33 days — fix public in source before the advisory

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 the chrome.debugger API.
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.
Key insight
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

  1. 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.
  2. Enable tracking The client calls Storage.setInterestGroupTracking(true); pre-patch this registers a profile-wide observer, delivering interestGroupAccessed events for Protected Audience activity across all tabs.
  3. Request details The client calls Storage.getInterestGroupDetails for a target owner origin; pre-patch the handler returns the group’s ads, bidding logic URL, and joining origin with no MayAttachToURL() check.
  4. 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

A DevTools client such as an extension could obtain Protected Audience bidding data and joining-origin/browsing-history signals from domains enterprise policy blocked it from, and could monitor interest-group activity across every tab in the profile, because the details command skipped 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

FunctionChangeNotes
if
content/browser/devtools/protocol/storage_handler.cc
modified

Files Changed

  • content/browser/devtools/protocol/storage_handler.cc
  • third_party/blink/web_tests/http/tests/inspector-protocol/storage/interest-groups-expected.txt

Audit Directions

  • CDP handlers missing attach checks
    Audit StorageHandler and other DevTools domain handlers for data-returning commands that omit MayAttachToURL()/policy checks reachable by extension-based clients.
  • Profile-wide observers from per-client commands
    Flag commands that register observers on profile-scoped managers (like InterestGroupManagerImpl), which can leak activity beyond the client’s permitted tab/origin scope.
  • Deprecated-API surface
    Review CDP commands backing deprecated features, since they often retain unmaintained data-exposure paths that bypass newer access controls.
From bf745058e2e8a43f342c49d5b985d793f0818bfb Mon Sep 17 00:00:00 2001
From: Danil Somsikov <[email protected]>
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 <[email protected]>
Auto-Submit: Danil Somsikov <[email protected]>
Reviewed-by: Andrey Kosyakov <[email protected]>
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
Loading diff…

Regression Test / PoC

shipped with the fix
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 {
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • content/browser/interest_group/interest_group_manager_impl.cc
  • content/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

  1. Profile-Wide Observation: In content/browser/devtools/protocol/storage_handler.cc, the SetInterestGroupTrackingInternal method registers the StorageHandler as an observer on InterestGroupManagerImpl. Because InterestGroupManagerImpl is scoped to the StoragePartition (the entire profile), the observer listens for all interest group events globally.
  2. Unfiltered Broadcasting: When an interest group event occurs in any tab, InterestGroupManagerImpl::NotifyInterestGroupAccessed iterates through all observers unconditionally. It does not filter by RenderFrameHost or tab ID. The StorageHandler::OnInterestGroupAccessed then forwards the event details (including owner_origin and name) to the DevTools frontend.
  3. Missing Authorization Check: The StorageHandler::GetInterestGroupDetails method retrieves the full interest group configuration using an (owner_origin, name) pair. Unlike other sensitive methods in StorageHandler (e.g., GetCookies, which filters results through NetworkHandler::CanAccessCookie and client_->MayAttachToURL()), GetInterestGroupDetails entirely omits the MayAttachToURL() 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.

  1. An attacker creates a Chrome extension with the debugger permission.
  2. The victim installs the extension in an environment with enterprise policies blocking access to restricted-domain.com.
  3. The extension attaches its debugger to a benign, permitted tab: chrome.debugger.attach({tabId: allowedTabId}, '1.3').
  4. The extension enables interest group tracking: chrome.debugger.sendCommand({tabId: allowedTabId}, 'Storage.setInterestGroupTracking', {enable: true}).
  5. The extension listens for chrome.debugger.onEvent with the method Storage.interestGroupAccessed.
  6. The victim opens a new tab and navigates to restricted-domain.com, which calls navigator.joinAdInterestGroup().
  7. The extension receives the event containing the ownerOrigin and name from the restricted tab.
  8. The extension uses the observed ownerOrigin and name to call Storage.getInterestGroupDetails, successfully retrieving the full interest group data, including joiningOrigin and userBiddingSignals, bypassing the enterprise blocklist.

Suggested Fix

  1. Add Permission Checks: In StorageHandler::GetInterestGroupDetails, verify the requested owner_origin against client_->MayAttachToURL() before querying the database, similar to how cookies are handled.
  2. Filter Observer Events: InterestGroupManagerImpl or StorageHandler should filter interest group access events. Events should only be dispatched to DevTools clients that are attached to the RenderFrameHost or WebContents where the event originated, or at a minimum, the owner_origin of the event should be checked against MayAttachToURL() before being forwarded in StorageHandler::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.

View on issue tracker