CVE-2026-113328
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
IN_PROC_BROWSER_TEST_Fchrome/browser/browsing_data/browsing_data_remover_browsertest.cc |
modified |
Files Changed
chrome/browser/browsing_data/chrome_browsing_data_remover_delegate.ccchrome/browser/browsing_data/browsing_data_remover_browsertest.ccchrome/browser/browsing_data/chrome_browsing_data_remover_delegate_unittest.cc
Patch
From b51618bbc9eb090a615cf2c30c35a4a0bb3b00e2 Mon Sep 17 00:00:00 2001
From: Guido Urdaneta <guidou@chromium.org>
Date: Thu, 17 Sep 2026 10:22:15 -0700
Subject: [PATCH] Preserve media device salts during partitioned cookie deletion
When clearing browsing data with a filter configured for partitioned
cookies only (such as via Clear-Site-Data in partitioned contexts or
bounce tracking mitigations), ChromeBrowsingDataRemoverDelegate invoked
MediaDeviceSaltService::DeleteSalts() without verifying the partition
scope. Because storage key domain matching can match first-party
storage keys, unpartitioned media device salts were inadvertently
deleted.
This CL updates ChromeBrowsingDataRemoverDelegate to check
filter_builder->PartitionedCookiesOnly() before invoking DeleteSalts,
aligning it with other embedder cookie data handlers and preserving
media device salts when only partitioned cookies are cleared.
Fix taken from https://crbug.com/518055862#comment6
Bug: 518055862
Change-Id: Id4bbff688d91313151162c1a9e8aed2f8b45c387
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8412570
Reviewed-by: Christian Dullweber <dullweber@chromium.org>
Commit-Queue: Guido Urdaneta <guidou@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1699935}
---
diff --git a/chrome/browser/browsing_data/chrome_browsing_data_remover_delegate.cc b/chrome/browser/browsing_data/chrome_browsing_data_remover_delegate.cc
index bdb99adb..daf4922 100644
--- a/chrome/browser/browsing_data/chrome_browsing_data_remover_delegate.cc
+++ b/chrome/browser/browsing_data/chrome_browsing_data_remover_delegate.cc
@@ -671,16 +671,19 @@
delegate->ClearPersistedTokens();
}
- if (auto* media_device_salt_service =
- MediaDeviceSaltServiceFactory::GetInstance()->GetForBrowserContext(
- profile_)) {
- content::StoragePartition::StorageKeyMatcherFunction storage_key_matcher;
- if (!filter_builder->MatchesAllOriginsAndDomains()) {
- storage_key_matcher = filter_builder->BuildStorageKeyFilter();
+ if (!filter_builder->PartitionedCookiesOnly()) {
+ if (auto* media_device_salt_service =
+ MediaDeviceSaltServiceFactory::GetInstance()
+ ->GetForBrowserContext(profile_)) {
+ content::StoragePartition::StorageKeyMatcherFunction
+ storage_key_matcher;
+ if (!filter_builder->MatchesAllOriginsAndDomains()) {
+ storage_key_matcher = filter_builder->BuildStorageKeyFilter();
+ }
+ media_device_salt_service->DeleteSalts(
+ delete_begin_, delete_end_, std::move(storage_key_matcher),
+ CreateTaskCompletionClosure(TracingDataType::kMediaDeviceSalts));
}
- media_device_salt_service->DeleteSalts(
- delete_begin_, delete_end_, std::move(storage_key_matcher),
- CreateTaskCompletionClosure(TracingDataType::kMediaDeviceSalts));
}
#if !BUILDFLAG(IS_ANDROID)
diff --git a/chrome/browser/browsing_data/browsing_data_remover_browsertest.cc b/chrome/browser/browsing_data/browsing_data_remover_browsertest.cc
index 40f882d6..f804e3d18 100644
--- a/chrome/browser/browsing_data/browsing_data_remover_browsertest.cc
+++ b/chrome/browser/browsing_data/browsing_data_remover_browsertest.cc
@@ -441,6 +441,83 @@
EXPECT_NE(original_salt, new_salt);
}
+// Test that the salt for media device IDs is not reset when only partitioned
+// cookies are cleared.
+IN_PROC_BROWSER_TEST_F(BrowsingDataRemoverBrowserTest,
+ MediaDeviceIdSalt_PartitionedCookiesOnly) {
+ GURL url = embedded_test_server()->GetURL(kExampleHost, "/simple_page.html");
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(GetBrowser(), url));
+
+ content::RenderFrameHost* frame_host = GetBrowser()
+ ->tab_strip_model()
+ ->GetActiveWebContents()
+ ->GetPrimaryMainFrame();
+ url::Origin origin = frame_host->GetLastCommittedOrigin();
+ net::SiteForCookies site_for_cookies =
+ net::SiteForCookies::FromOrigin(origin);
+ blink::StorageKey storage_key = blink::StorageKey::CreateFirstParty(origin);
+
+ base::test::TestFuture<bool, const std::string&> future;
+ content::GetContentClientForTesting()->browser()->GetMediaDeviceIDSalt(
+ frame_host, site_for_cookies, storage_key, future.GetCallback());
+ std::string original_salt = future.Get<1>();
+
+ std::unique_ptr<content::BrowsingDataFilterBuilder> filter_builder =
+ content::BrowsingDataFilterBuilder::Create(
+ content::BrowsingDataFilterBuilder::Mode::kDelete);
+ filter_builder->AddRegisterableDomain(kExampleHost);
+ filter_builder->SetCookiePartitionKeyCollection(
+ net::CookiePartitionKeyCollection(
+ net::CookiePartitionKey::FromURLForTesting(
+ GURL("https://other-site.com"))));
+ filter_builder->SetPartitionedCookiesOnly(true);
+
+ RemoveWithFilterAndWait(content::BrowsingDataRemover::DATA_TYPE_COOKIES,
+ std::move(filter_builder));
+
+ future.Clear();
+ content::GetContentClientForTesting()->browser()->GetMediaDeviceIDSalt(
+ frame_host, site_for_cookies, storage_key, future.GetCallback());
+ std::string new_salt = future.Get<1>();
+
+ EXPECT_EQ(original_salt, new_salt);
+}
+
+// Test that Clear-Site-Data for partitioned cookies does not reset the
+// first-party salt for media device IDs.
+IN_PROC_BROWSER_TEST_F(
+ BrowsingDataRemoverBrowserTest,
+ ClearSiteDataPartitionedCookiesDoesNotResetMediaDeviceIdSalt) {
+ GURL url = embedded_test_server()->GetURL(kExampleHost, "/simple_page.html");
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(GetBrowser(), url));
+
+ content::RenderFrameHost* frame_host = GetBrowser()
+ ->tab_strip_model()
+ ->GetActiveWebContents()
+ ->GetPrimaryMainFrame();
+ url::Origin origin = frame_host->GetLastCommittedOrigin();
+ net::SiteForCookies site_for_cookies =
+ net::SiteForCookies::FromOrigin(origin);
+ blink::StorageKey storage_key = blink::StorageKey::CreateFirstParty(origin);
+
+ base::test::TestFuture<bool, const std::string&> future;
+ content::GetContentClientForTesting()->browser()->GetMediaDeviceIDSalt(
+ frame_host, site_for_cookies, storage_key, future.GetCallback());
+ std::string original_salt = future.Get<1>();
+
+ net::CookiePartitionKey partition_key =
+ net::CookiePartitionKey::FromURLForTesting(GURL("https://other.example"));
+ ClearSiteDataAndWait(origin, partition_key, /*storage_key=*/std::nullopt,
+ /*storage_buckets_to_remove=*/{});
+
+ future.Clear();
+ content::GetContentClientForTesting()->browser()->GetMediaDeviceIDSalt(
+ frame_host, site_for_cookies, storage_key, future.GetCallback());
+ std::string new_salt = future.Get<1>();
+
+ EXPECT_EQ(original_salt, new_salt);
+}
+
#if BUILDFLAG(ENABLE_DICE_SUPPORT)
// Test that Sync is not paused when cookies are cleared.
IN_PROC_BROWSER_TEST_F(DiceBrowsingDataRemoverBrowserTest, SyncToken) {
diff --git a/chrome/browser/browsing_data/chrome_browsing_data_remover_delegate_unittest.cc b/chrome/browser/browsing_data/chrome_browsing_data_remover_delegate_unittest.cc
index b959b16..a3f9f54 100644
--- a/chrome/browser/browsing_data/chrome_browsing_data_remover_delegate_unittest.cc
+++ b/chrome/browser/browsing_data/chrome_browsing_data_remover_delegate_unittest.cc
@@ -4605,6 +4605,46 @@
EXPECT_EQ(salt3c, salt3b);
}
+TEST_F(ChromeBrowsingDataRemoverDelegateMediaDeviceSaltTest,
+ PartitionedCookiesOnlyDoesNotRemoveSalts) {
+ std::string salt1 = GetSalt(StorageKey1());
+ std::string salt2 = GetSalt(StorageKey2());
+ std::string salt3 = GetSalt(StorageKey3());
+
+ std::unique_ptr<BrowsingDataFilterBuilder> filter(
+ BrowsingDataFilterBuilder::Create(
+ BrowsingDataFilterBuilder::Mode::kDelete));
+ filter->AddRegisterableDomain(StorageKey1().origin().host());
+ filter->SetCookiePartitionKeyCollection(net::CookiePartitionKeyCollection(
+ net::CookiePartitionKey::FromURLForTesting(
+ GURL("https://example2.com"))));
+ filter->SetPartitionedCookiesOnly(true);
+ BlockUntilOriginDataRemoved(base::Time(), base::Time::Max(),
+ content::BrowsingDataRemover::DATA_TYPE_COOKIES,
+ std::move(filter));
+ EXPECT_EQ(GetSalt(StorageKey1()), salt1);
+ EXPECT_EQ(GetSalt(StorageKey2()), salt2);
+ EXPECT_EQ(GetSalt(StorageKey3()), salt3);
+}
+
+TEST_F(ChromeBrowsingDataRemoverDelegateMediaDeviceSaltTest,
+ PartitionedCookiesOnlyPreserveModeDoesNotRemoveSalts) {
+ std::string salt1 = GetSalt(StorageKey1());
+ std::string salt2 = GetSalt(StorageKey2());
+ std::string salt3 = GetSalt(StorageKey3());
+
+ std::unique_ptr<BrowsingDataFilterBuilder> filter(
+ BrowsingDataFilterBuilder::Create(
+ BrowsingDataFilterBuilder::Mode::kPreserve));
+ filter->SetPartitionedCookiesOnly(true);
+ BlockUntilOriginDataRemoved(base::Time(), base::Time::Max(),
+ content::BrowsingDataRemover::DATA_TYPE_COOKIES,
+ std::move(filter));
+ EXPECT_EQ(GetSalt(StorageKey1()), salt1);
+ EXPECT_EQ(GetSalt(StorageKey2()), salt2);
+ EXPECT_EQ(GetSalt(StorageKey3()), salt3);
+}
+
// Constants for ChromeBrowsingDataRemoverDelegateRelatedWebsiteSetsTest.
namespace {
constexpr char kPrimaryUrl[] = "https://subdomain.example.com:112";
Regression Test / PoC
diff --git a/chrome/browser/browsing_data/browsing_data_remover_browsertest.cc b/chrome/browser/browsing_data/browsing_data_remover_browsertest.cc
index 40f882d6..f804e3d18 100644
--- a/chrome/browser/browsing_data/browsing_data_remover_browsertest.cc
+++ b/chrome/browser/browsing_data/browsing_data_remover_browsertest.cc
@@ -441,6 +441,83 @@
EXPECT_NE(original_salt, new_salt);
}
+// Test that the salt for media device IDs is not reset when only partitioned
+// cookies are cleared.
+IN_PROC_BROWSER_TEST_F(BrowsingDataRemoverBrowserTest,
+ MediaDeviceIdSalt_PartitionedCookiesOnly) {
+ GURL url = embedded_test_server()->GetURL(kExampleHost, "/simple_page.html");
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(GetBrowser(), url));
+
+ content::RenderFrameHost* frame_host = GetBrowser()
+ ->tab_strip_model()
+ ->GetActiveWebContents()
+ ->GetPrimaryMainFrame();
+ url::Origin origin = frame_host->GetLastCommittedOrigin();
+ net::SiteForCookies site_for_cookies =
+ net::SiteForCookies::FromOrigin(origin);
+ blink::StorageKey storage_key = blink::StorageKey::CreateFirstParty(origin);
+
+ base::test::TestFuture<bool, const std::string&> future;
+ content::GetContentClientForTesting()->browser()->GetMediaDeviceIDSalt(
+ frame_host, site_for_cookies, storage_key, future.GetCallback());
+ std::string original_salt = future.Get<1>();
+
+ std::unique_ptr<content::BrowsingDataFilterBuilder> filter_builder =
+ content::BrowsingDataFilterBuilder::Create(
+ content::BrowsingDataFilterBuilder::Mode::kDelete);
+ filter_builder->AddRegisterableDomain(kExampleHost);
+ filter_builder->SetCookiePartitionKeyCollection(
+ net::CookiePartitionKeyCollection(
+ net::CookiePartitionKey::FromURLForTesting(
+ GURL("https://other-site.com"))));
+ filter_builder->SetPartitionedCookiesOnly(true);
+
+ RemoveWithFilterAndWait(content::BrowsingDataRemover::DATA_TYPE_COOKIES,
+ std::move(filter_builder));
+
+ future.Clear();
+ content::GetContentClientForTesting()->browser()->GetMediaDeviceIDSalt(
+ frame_host, site_for_cookies, storage_key, future.GetCallback());
+ std::string new_salt = future.Get<1>();
+
+ EXPECT_EQ(original_salt, new_salt);
+}
+
+// Test that Clear-Site-Data for partitioned cookies does not reset the
+// first-party salt for media device IDs.
+IN_PROC_BROWSER_TEST_F(
+ BrowsingDataRemoverBrowserTest,
+ ClearSiteDataPartitionedCookiesDoesNotResetMediaDeviceIdSalt) {
+ GURL url = embedded_test_server()->GetURL(kExampleHost, "/simple_page.html");
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(GetBrowser(), url));
+
+ content::RenderFrameHost* frame_host = GetBrowser()
+ ->tab_strip_model()
+ ->GetActiveWebContents()
+ ->GetPrimaryMainFrame();
+ url::Origin origin = frame_host->GetLastCommittedOrigin();
+ net::SiteForCookies site_for_cookies =
+ net::SiteForCookies::FromOrigin(origin);
+ blink::StorageKey storage_key = blink::StorageKey::CreateFirstParty(origin);
+
+ base::test::TestFuture<bool, const std::string&> future;
+ content::GetContentClientForTesting()->browser()->GetMediaDeviceIDSalt(
+ frame_host, site_for_cookies, storage_key, future.GetCallback());
+ std::string original_salt = future.Get<1>();
+
+ net::CookiePartitionKey partition_key =
+ net::CookiePartitionKey::FromURLForTesting(GURL("https://other.example"));
+ ClearSiteDataAndWait(origin, partition_key, /*storage_key=*/std::nullopt,
+ /*storage_buckets_to_remove=*/{});
+
+ future.Clear();
+ content::GetContentClientForTesting()->browser()->GetMediaDeviceIDSalt(
+ frame_host, site_for_cookies, storage_key, future.GetCallback());
+ std::string new_salt = future.Get<1>();
+
+ EXPECT_EQ(original_salt, new_salt);
+}
+
#if BUILDFLAG(ENABLE_DICE_SUPPORT)
// Test that Sync is not paused when cookies are cleared.
IN_PROC_BROWSER_TEST_F(DiceBrowsingDataRemoverBrowserTest, SyncToken) {
diff --git a/chrome/browser/browsing_data/chrome_browsing_data_remover_delegate_unittest.cc b/chrome/browser/browsing_data/chrome_browsing_data_remover_delegate_unittest.cc
index b959b16..a3f9f54 100644
--- a/chrome/browser/browsing_data/chrome_browsing_data_remover_delegate_unittest.cc
+++ b/chrome/browser/browsing_data/chrome_browsing_data_remover_delegate_unittest.cc
@@ -4605,6 +4605,46 @@
EXPECT_EQ(salt3c, salt3b);
}
+TEST_F(ChromeBrowsingDataRemoverDelegateMediaDeviceSaltTest,
+ PartitionedCookiesOnlyDoesNotRemoveSalts) {
+ std::string salt1 = GetSalt(StorageKey1());
+ std::string salt2 = GetSalt(StorageKey2());
+ std::string salt3 = GetSalt(StorageKey3());
+
+ std::unique_ptr<BrowsingDataFilterBuilder> filter(
+ BrowsingDataFilterBuilder::Create(
+ BrowsingDataFilterBuilder::Mode::kDelete));
+ filter->AddRegisterableDomain(StorageKey1().origin().host());
+ filter->SetCookiePartitionKeyCollection(net::CookiePartitionKeyCollection(
+ net::CookiePartitionKey::FromURLForTesting(
+ GURL("https://example2.com"))));
+ filter->SetPartitionedCookiesOnly(true);
+ BlockUntilOriginDataRemoved(base::Time(), base::Time::Max(),
+ content::BrowsingDataRemover::DATA_TYPE_COOKIES,
+ std::move(filter));
+ EXPECT_EQ(GetSalt(StorageKey1()), salt1);
+ EXPECT_EQ(GetSalt(StorageKey2()), salt2);
+ EXPECT_EQ(GetSalt(StorageKey3()), salt3);
+}
+
+TEST_F(ChromeBrowsingDataRemoverDelegateMediaDeviceSaltTest,
+ PartitionedCookiesOnlyPreserveModeDoesNotRemoveSalts) {
+ std::string salt1 = GetSalt(StorageKey1());
+ std::string salt2 = GetSalt(StorageKey2());
+ std::string salt3 = GetSalt(StorageKey3());
+
+ std::unique_ptr<BrowsingDataFilterBuilder> filter(
+ BrowsingDataFilterBuilder::Create(
+ BrowsingDataFilterBuilder::Mode::kPreserve));
+ filter->SetPartitionedCookiesOnly(true);
+ BlockUntilOriginDataRemoved(base::Time(), base::Time::Max(),
+ content::BrowsingDataRemover::DATA_TYPE_COOKIES,
+ std::move(filter));
+ EXPECT_EQ(GetSalt(StorageKey1()), salt1);
+ EXPECT_EQ(GetSalt(StorageKey2()), salt2);
+ EXPECT_EQ(GetSalt(StorageKey3()), salt3);
+}
+
// Constants for ChromeBrowsingDataRemoverDelegateRelatedWebsiteSetsTest.
namespace {
constexpr char kPrimaryUrl[] = "https://subdomain.example.com:112";
Original Bug Report
Deletion of first-party MediaDeviceSalts by Clear-Site-Data from partitioned contexts
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: Processing a Clear-Site-Data: “cookies” header in a partitioned third-party context potentially clears first-party MediaDeviceSalts. This occurs because ChromeBrowsingDataRemoverDelegate lacks a PartitionedCookiesOnly check when clearing media device salts, resulting in domain-scoped deletions matching first-party storage keys. This could allow third-party trackers to establish a cross-partition tracking channel using rotated persistent device IDs.
Affected files:
chrome/browser/browsing_data/chrome_browsing_data_remover_delegate.cccontent/browser/browsing_data/browsing_data_filter_builder_impl.cc
Estimated timestamp from git blame: 2023-07-21
Potential Root Cause Analysis
In ChromeBrowsingDataRemoverDelegate::RemoveEmbedderData (chrome/browser/browsing_data/chrome_browsing_data_remover_delegate.cc), the deletion of media device salts is performed as part of the DATA_TYPE_COOKIES clearing block. However, unlike other sibling cookie deletions, it lacks a check for PartitionedCookiesOnly():
// chrome/browser/browsing_data/chrome_browsing_data_remover_delegate.cc
if (auto* media_device_salt_service =
MediaDeviceSaltServiceFactory::GetInstance()->GetForBrowserContext(
profile_)) {
content::StoragePartition::StorageKeyMatcherFunction storage_key_matcher;
if (!filter_builder->MatchesAllOriginsAndDomains()) {
storage_key_matcher = filter_builder->BuildStorageKeyFilter();
}
media_device_salt_service->DeleteSalts(
delete_begin_, delete_end_, std::move(storage_key_matcher),
CreateTaskCompletionClosure(TracingDataType::kMediaDeviceSalts));
}
When a partitioned/3PCD-blocked third-party frame (e.g., https://tracker.test) embedded on https://publisher.test returns a Clear-Site-Data: "cookies" header, the browser initiates data clearing with a BrowsingDataFilterBuilder configured with PartitionedCookiesOnly(true).
Sibling cookie-clearing calls are protected via if (!filter_builder->PartitionedCookiesOnly()). However, the DeleteSalts block is executed unconditionally. Since no explicit StorageKey is configured on the cookie filter builder, BuildStorageKeyFilter() (in content/browser/browsing_data/browsing_data_filter_builder_impl.cc) returns a matcher function based on MatchesStorageKey.
This matcher uses StorageKey::MatchesRegistrableDomainForTrustedStorageDeletion(domain), which evaluates to true for any StorageKey whose top_level_site_ has a matching registrable domain. This matches the first-party StorageKey(https://tracker.test, top=https://tracker.test), meaning the tracker’s first-party media device salt is deleted instead of only partitioned entries.
Potential Steps to Reproduce
Note: The following are suggested/potential reproduction steps, as our testing environment does not currently support live code execution or browser automation.
- Navigate to
https://tracker.testas a first-party site, grant camera/microphone permissions, and runnavigator.mediaDevices.enumerateDevices()to obtain persistent device IDs. - Enable third-party-cookie blocking (Chrome’s 3PCD setting).
- Navigate to
https://publisher.testwhich triggers a subresource request tohttps://tracker.test/px. - Configure
https://tracker.test/pxto respond withClear-Site-Data: "cookies". - Navigate back to
https://tracker.testas a first-party site, callenumerateDevices()again, and check if the device IDs have rotated. (If a first-party tab was open during step 4, check if it received an unexpecteddevicechangeevent viabase::SystemMonitor).
Suggested Fix
Prevent clearing unpartitioned media device salts during partitioned cookie-clearing operations by wrapping the service call in a !filter_builder->PartitionedCookiesOnly() check in chrome/browser/browsing_data/chrome_browsing_data_remover_delegate.cc:
if (!filter_builder->PartitionedCookiesOnly()) {
if (auto* media_device_salt_service =
MediaDeviceSaltServiceFactory::GetInstance()->GetForBrowserContext(
profile_)) {
content::StoragePartition::StorageKeyMatcherFunction storage_key_matcher;
if (!filter_builder->MatchesAllOriginsAndDomains()) {
storage_key_matcher = filter_builder->BuildStorageKeyFilter();
}
media_device_salt_service->DeleteSalts(
delete_begin_, delete_end_, std::move(storage_key_matcher),
CreateTaskCompletionClosure(TracingDataType::kMediaDeviceSalts));
}
}
Evaluated with Chrome root at commit: fb72408a8493c46bc75fae1c70d03daec96b3040
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.