Chrome · Media
CVE-2026-113085
UAF in Media
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/media/media_storage_id_salt.cc |
modified | |
PrefRegistrySimplechrome/browser/media/media_storage_id_salt.h |
modified | |
PrefServicechrome/browser/media/media_storage_id_salt.h |
modified | |
Profilechrome/browser/media/media_storage_id_salt.h |
modified | |
MediaStorageIdSaltchrome/browser/media/media_storage_id_salt.h |
modified | |
TESTchrome/browser/media/media_storage_id_salt_unittest.cc |
modified | |
MediaStorageIdSaltTestchrome/browser/media/media_storage_id_salt_unittest.cc |
modified | |
TEST_Fchrome/browser/media/media_storage_id_salt_unittest.cc |
modified |
Files Changed
chrome/browser/media/cdm_document_service_impl.ccchrome/browser/media/media_storage_id_salt.ccchrome/browser/media/media_storage_id_salt_unittest.ccchrome/browser/media/media_storage_id_salt.h
Patch
From 5d6f6f98ace1275c2ebe0439df4c26592edefeaa Mon Sep 17 00:00:00 2001 From: Feras Aldahlawi <[email protected]> Date: Tue, 15 Sep 2026 10:06:07 -0700 Subject: [PATCH] Fix cross-profile identifier leak in EME Storage ID via OTR profile In branded builds on Windows, macOS, and ChromeOS (enable_cdm_storage_id), CdmDocumentServiceImpl::GetStorageId computes the EME Storage ID from the salt returned by MediaStorageIdSalt. Previously, GetStorageIdSaltFromProfile passed profile->GetPrefs() directly to MediaStorageIdSalt::GetSalt(). For off-the-record profiles, the PrefService is backed by an OverlayUserPrefStore over the parent regular profile's persistent store. When reading media.storage_id_salt, the read falls through to the regular profile's persisted salt if one exists. Because the EME Storage ID is computed as SHA256(machine_id + origin + key + profile_salt), an Incognito CDM received a Storage ID byte-identical to the one the same origin receives in regular browsing mode. Bug: 559776316 Change-Id: Ia75a20d2a9d351ebe6236b63c029a5696a6a6964 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8408727 Reviewed-by: Frank Liberato <[email protected]> Commit-Queue: Feras Aldahlawi <[email protected]> Cr-Commit-Position: refs/heads/main@{#1698212} --- diff --git a/chrome/browser/media/cdm_document_service_impl.cc b/chrome/browser/media/cdm_document_service_impl.cc index c2cdf30..019489e5 100644 --- a/chrome/browser/media/cdm_document_service_impl.cc +++ b/chrome/browser/media/cdm_document_service_impl.cc @@ -71,7 +71,7 @@ DCHECK(rfh); Profile* profile = Profile::FromBrowserContext(rfh->GetProcess()->GetBrowserContext()); - return MediaStorageIdSalt::GetSalt(profile->GetPrefs()); + return MediaStorageIdSalt::GetSalt(profile); } #endif // BUILDFLAG(ENABLE_CDM_STORAGE_ID) diff --git a/chrome/browser/media/media_storage_id_salt.cc b/chrome/browser/media/media_storage_id_salt.cc index eeca490..066ce36 100644 --- a/chrome/browser/media/media_storage_id_salt.cc +++ b/chrome/browser/media/media_storage_id_salt.cc @@ -5,37 +5,80 @@ #include "chrome/browser/media/media_storage_id_salt.h" #include <string> +#include <vector> #include "base/logging.h" #include "base/strings/string_number_conversions.h" +#include "base/supports_user_data.h" +#include "chrome/browser/profiles/profile.h" #include "chrome/common/pref_names.h" #include "components/prefs/pref_registry_simple.h" #include "components/prefs/pref_service.h" #include "crypto/random.h" -std::vector<uint8_t> MediaStorageIdSalt::GetSalt(PrefService* pref_service) { +namespace { + +struct MediaStorageIdSaltUserData : public base::SupportsUserData::Data { + explicit MediaStorageIdSaltUserData(std::vector<uint8_t> salt) + : salt(std::move(salt)) {} + std::vector<uint8_t> salt; +}; + +const void* const kMediaStorageIdSaltUserDataKey = + &kMediaStorageIdSaltUserDataKey; + +std::vector<uint8_t> GetSaltFromPrefs(PrefService* pref_service) { // Salt is stored as hex-encoded string. std::string encoded_salt = pref_service->GetString(prefs::kMediaStorageIdSalt); std::vector<uint8_t> salt; - if (encoded_salt.length() != kSaltLength * 2 || - !base::HexStringToBytes(encoded_salt, &salt)) { - // If the salt is not the proper format log an error. - if (encoded_salt.length() > 0) { - DLOG(ERROR) << "Saved value for " << prefs::kMediaStorageIdSalt - << " is not valid: " << encoded_salt; - // Continue on to generate a new one. - } - - // If the salt doesn't exist, generate a new one. - salt = crypto::RandBytesAsVector(kSaltLength); - encoded_salt = base::HexEncode(salt); - pref_service->SetString(prefs::kMediaStorageIdSalt, encoded_salt); + if (encoded_salt.length() == MediaStorageIdSalt::kSaltLength * 2 && + base::HexStringToBytes(encoded_salt, &salt)) { + return salt; } + if (!encoded_salt.empty()) { + DLOG(ERROR) << "Saved value for " << prefs::kMediaStorageIdSalt + << " is not valid: " << encoded_salt; + } + + // Generate and store a new salt. + salt = crypto::RandBytesAsVector(MediaStorageIdSalt::kSaltLength); + pref_service->SetString(prefs::kMediaStorageIdSalt, base::HexEncode(salt)); return salt; } +std::vector<uint8_t> GetSaltForOffTheRecordProfile(Profile* profile) { + auto* user_data = static_cast<MediaStorageIdSaltUserData*>( + profile->GetUserData(kMediaStorageIdSaltUserDataKey)); + if (user_data) { + return user_data->salt; + } + + std::vector<uint8_t> salt = + crypto::RandBytesAsVector(MediaStorageIdSalt::kSaltLength); + profile->SetUserData(kMediaStorageIdSaltUserDataKey, + std::make_unique<MediaStorageIdSaltUserData>(salt)); + return salt; +} + +} // namespace + +std::vector<uint8_t> MediaStorageIdSalt::GetSalt(Profile* profile) { + if (!profile) { + return {}; + } + + // Regular profiles persist the salt in user preferences. + if (!profile->IsOffTheRecord()) { + return GetSaltFromPrefs(profile->GetPrefs()); + } + + // Off-the-record profiles use an in-memory ephemeral salt to prevent + // falling through to the regular profile's persistent pref store. + return GetSaltForOffTheRecordProfile(profile); +} + void MediaStorageIdSalt::RegisterProfilePrefs(PrefRegistrySimple* registry) { registry->RegisterStringPref(prefs::kMediaStorageIdSalt, std::string()); } diff --git a/chrome/browser/media/media_storage_id_salt.h b/chrome/browser/media/media_storage_id_salt.h index 00cd6bc..16159a2 100644 --- a/chrome/browser/media/media_storage_id_salt.h +++ b/chrome/browser/media/media_storage_id_salt.h @@ -10,9 +10,9 @@ #include <vector> class PrefRegistrySimple; -class PrefService; +class Profile; -// MediaStorageIDSalt is responsible for creating and retrieving a salt string +// MediaStorageIdSalt is responsible for creating and retrieving a salt string // that is used when creating Storage IDs. class MediaStorageIdSalt { public: @@ -22,8 +22,10 @@ MediaStorageIdSalt(const MediaStorageIdSalt&) = delete; MediaStorageIdSalt& operator=(const MediaStorageIdSalt&) = delete; - // Retrieves the current salt. If one does not currently exist it is created. - static std::vector<uint8_t> GetSalt(PrefService* pref_service); + // Retrieves the current salt for `profile`. If one does not currently exist + // it is created. For off-the-record profiles, an ephemeral in-memory salt is + // used. + static std::vector<uint8_t> GetSalt(Profile* profile); static void RegisterProfilePrefs(PrefRegistrySimple* registry); }; diff --git a/chrome/browser/media/media_storage_id_salt_unittest.cc b/chrome/browser/media/media_storage_id_salt_unittest.cc index 2dbd2bb1..7903bee67 100644 --- a/chrome/browser/media/media_storage_id_salt_unittest.cc +++ b/chrome/browser/media/media_storage_id_salt_unittest.cc @@ -4,51 +4,89 @@ #include "chrome/browser/media/media_storage_id_salt.h" +#include <string> +#include <vector> + +#include "chrome/browser/profiles/profile.h" #include "chrome/common/pref_names.h" +#include "chrome/test/base/testing_profile.h" #include "components/prefs/testing_pref_service.h" +#include "content/public/test/browser_task_environment.h" #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" -TEST(MediaStorageIdSalt, Register) { +class MediaStorageIdSaltTest : public testing::Test { + protected: + content::BrowserTaskEnvironment task_environment_; + TestingProfile profile_; +}; + +TEST_F(MediaStorageIdSaltTest, Register) { TestingPrefServiceSimple prefs; MediaStorageIdSalt::RegisterProfilePrefs(prefs.registry()); } -TEST(MediaStorageIdSalt, Create) { - TestingPrefServiceSimple prefs; - - MediaStorageIdSalt::RegisterProfilePrefs(prefs.registry()); - std::vector<uint8_t> salt = MediaStorageIdSalt::GetSalt(&prefs); +TEST_F(MediaStorageIdSaltTest, Create) { + std::vector<uint8_t> salt = MediaStorageIdSalt::GetSalt(&profile_); EXPECT_EQ(static_cast<size_t>(MediaStorageIdSalt::kSaltLength), salt.size()); } -TEST(MediaStorageIdSalt, Recreate) { - TestingPrefServiceSimple prefs; - - MediaStorageIdSalt::RegisterProfilePrefs(prefs.registry()); - std::vector<uint8_t> original_salt = MediaStorageIdSalt::GetSalt(&prefs); +TEST_F(MediaStorageIdSaltTest, Recreate) { + std::vector<uint8_t> original_salt = MediaStorageIdSalt::GetSalt(&profile_); EXPECT_EQ(static_cast<size_t>(MediaStorageIdSalt::kSaltLength), original_salt.size()); // Now that the salt is created, mess it up and then try fetching it again // (should generate a new salt and log an error). - prefs.SetString(prefs::kMediaStorageIdSalt, "123"); - std::vector<uint8_t> new_salt = MediaStorageIdSalt::GetSalt(&prefs); + profile_.GetPrefs()->SetString(prefs::kMediaStorageIdSalt, "123"); + std::vector<uint8_t> new_salt = MediaStorageIdSalt::GetSalt(&profile_); EXPECT_EQ(static_cast<size_t>(MediaStorageIdSalt::kSaltLength), new_salt.size()); EXPECT_NE(original_salt, new_salt); } -TEST(MediaStorageIdSalt, FetchTwice) { - TestingPrefServiceSimple prefs; - - MediaStorageIdSalt::RegisterProfilePrefs(prefs.registry()); - std::vector<uint8_t> salt1 = MediaStorageIdSalt::GetSalt(&prefs); +TEST_F(MediaStorageIdSaltTest, FetchTwice) { + std::vector<uint8_t> salt1 = MediaStorageIdSalt::GetSalt(&profile_); EXPECT_EQ(static_cast<size_t>(MediaStorageIdSalt::kSaltLength), salt1.size()); // Fetch the salt again. Should be the same value. - std::vector<uint8_t> salt2 = MediaStorageIdSalt::GetSalt(&prefs); + std::vector<uint8_t> salt2 = MediaStorageIdSalt::GetSalt(&profile_); EXPECT_EQ(static_cast<size_t>(MediaStorageIdSalt::kSaltLength), salt2.size()); EXPECT_EQ(salt1, salt2); } + +TEST_F(MediaStorageIdSaltTest, OffTheRecord) { + std::vector<uint8_t> regular_salt = MediaStorageIdSalt::GetSalt(&profile_); + EXPECT_EQ(static_cast<size_t>(MediaStorageIdSalt::kSaltLength), + regular_salt.size()); + + TestingProfile* incognito_profile = + TestingProfile::Builder().BuildIncognito(&profile_); + std::vector<uint8_t> incognito_salt1 = + MediaStorageIdSalt::GetSalt(incognito_profile); + EXPECT_EQ(static_cast<size_t>(MediaStorageIdSalt::kSaltLength), + incognito_salt1.size()); + EXPECT_NE(regular_salt, incognito_salt1); + + // Calling again on the same Incognito profile should return the same salt. + std::vector<uint8_t> incognito_salt2 = + MediaStorageIdSalt::GetSalt(incognito_profile); + EXPECT_EQ(incognito_salt1, incognito_salt2); +} + +TEST_F(MediaStorageIdSaltTest, IncognitoFirstDoesNotLeakToRegular) { + TestingProfile* incognito_profile = + TestingProfile::Builder().BuildIncognito(&profile_); + std::vector<uint8_t> incognito_salt = + MediaStorageIdSalt::GetSalt(incognito_profile); + EXPECT_EQ(static_cast<size_t>(MediaStorageIdSalt::kSaltLength), + incognito_salt.size()); + + // Fetching the regular profile's salt after Incognito should generate a + // distinct salt and not reuse or leak the Incognito salt. + std::vector<uint8_t> regular_salt = MediaStorageIdSalt::GetSalt(&profile_); + EXPECT_EQ(static_cast<size_t>(MediaStorageIdSalt::kSaltLength), + regular_salt.size()); + EXPECT_NE(incognito_salt, regular_salt); +}
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/chrome/browser/media/media_storage_id_salt_unittest.cc b/chrome/browser/media/media_storage_id_salt_unittest.cc
index 2dbd2bb1..7903bee67 100644
--- a/chrome/browser/media/media_storage_id_salt_unittest.cc
+++ b/chrome/browser/media/media_storage_id_salt_unittest.cc
@@ -4,51 +4,89 @@
#include "chrome/browser/media/media_storage_id_salt.h"
+#include <string>
+#include <vector>
+
+#include "chrome/browser/profiles/profile.h"
#include "chrome/common/pref_names.h"
+#include "chrome/test/base/testing_profile.h"
#include "components/prefs/testing_pref_service.h"
+#include "content/public/test/browser_task_environment.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
-TEST(MediaStorageIdSalt, Register) {
+class MediaStorageIdSaltTest : public testing::Test {
+ protected:
+ content::BrowserTaskEnvironment task_environment_;
+ TestingProfile profile_;
+};
+
+TEST_F(MediaStorageIdSaltTest, Register) {
TestingPrefServiceSimple prefs;
MediaStorageIdSalt::RegisterProfilePrefs(prefs.registry());
}
-TEST(MediaStorageIdSalt, Create) {
- TestingPrefServiceSimple prefs;
-
- MediaStorageIdSalt::RegisterProfilePrefs(prefs.registry());
- std::vector<uint8_t> salt = MediaStorageIdSalt::GetSalt(&prefs);
+TEST_F(MediaStorageIdSaltTest, Create) {
+ std::vector<uint8_t> salt = MediaStorageIdSalt::GetSalt(&profile_);
EXPECT_EQ(static_cast<size_t>(MediaStorageIdSalt::kSaltLength), salt.size());
}
-TEST(MediaStorageIdSalt, Recreate) {
- TestingPrefServiceSimple prefs;
-
- MediaStorageIdSalt::RegisterProfilePrefs(prefs.registry());
- std::vector<uint8_t> original_salt = MediaStorageIdSalt::GetSalt(&prefs);
+TEST_F(MediaStorageIdSaltTest, Recreate) {
+ std::vector<uint8_t> original_salt = MediaStorageIdSalt::GetSalt(&profile_);
EXPECT_EQ(static_cast<size_t>(MediaStorageIdSalt::kSaltLength),
original_salt.size());
// Now that the salt is created, mess it up and then try fetching it again
// (should generate a new salt and log an error).
- prefs.SetString(prefs::kMediaStorageIdSalt, "123");
- std::vector<uint8_t> new_salt = MediaStorageIdSalt::GetSalt(&prefs);
+ profile_.GetPrefs()->SetString(prefs::kMediaStorageIdSalt, "123");
+ std::vector<uint8_t> new_salt = MediaStorageIdSalt::GetSalt(&profile_);
EXPECT_EQ(static_cast<size_t>(MediaStorageIdSalt::kSaltLength),
new_salt.size());
EXPECT_NE(original_salt, new_salt);
}
-TEST(MediaStorageIdSalt, FetchTwice) {
- TestingPrefServiceSimple prefs;
-
- MediaStorageIdSalt::RegisterProfilePrefs(prefs.registry());
- std::vector<uint8_t> salt1 = MediaStorageIdSalt::GetSalt(&prefs);
+TEST_F(MediaStorageIdSaltTest, FetchTwice) {
+ std::vector<uint8_t> salt1 = MediaStorageIdSalt::GetSalt(&profile_);
EXPECT_EQ(static_cast<size_t>(MediaStorageIdSalt::kSaltLength), salt1.size());
// Fetch the salt again. Should be the same value.
- std::vector<uint8_t> salt2 = MediaStorageIdSalt::GetSalt(&prefs);
+ std::vector<uint8_t> salt2 = MediaStorageIdSalt::GetSalt(&profile_);
EXPECT_EQ(static_cast<size_t>(MediaStorageIdSalt::kSaltLength), salt2.size());
EXPECT_EQ(salt1, salt2);
}
+
+TEST_F(MediaStorageIdSaltTest, OffTheRecord) {
+ std::vector<uint8_t> regular_salt = MediaStorageIdSalt::GetSalt(&profile_);
+ EXPECT_EQ(static_cast<size_t>(MediaStorageIdSalt::kSaltLength),
+ regular_salt.size());
+
+ TestingProfile* incognito_profile =
+ TestingProfile::Builder().BuildIncognito(&profile_);
+ std::vector<uint8_t> incognito_salt1 =
+ MediaStorageIdSalt::GetSalt(incognito_profile);
+ EXPECT_EQ(static_cast<size_t>(MediaStorageIdSalt::kSaltLength),
+ incognito_salt1.size());
+ EXPECT_NE(regular_salt, incognito_salt1);
+
+ // Calling again on the same Incognito profile should return the same salt.
+ std::vector<uint8_t> incognito_salt2 =
+ MediaStorageIdSalt::GetSalt(incognito_profile);
+ EXPECT_EQ(incognito_salt1, incognito_salt2);
+}
+
+TEST_F(MediaStorageIdSaltTest, IncognitoFirstDoesNotLeakToRegular) {
+ TestingProfile* incognito_profile =
+ TestingProfile::Builder().BuildIncognito(&profile_);
+ std::vector<uint8_t> incognito_salt =
+ MediaStorageIdSalt::GetSalt(incognito_profile);
+ EXPECT_EQ(static_cast<size_t>(MediaStorageIdSalt::kSaltLength),
+ incognito_salt.size());
+
+ // Fetching the regular profile's salt after Incognito should generate a
+ // distinct salt and not reuse or leak the Incognito salt.
+ std::vector<uint8_t> regular_salt = MediaStorageIdSalt::GetSalt(&profile_);
+ EXPECT_EQ(static_cast<size_t>(MediaStorageIdSalt::kSaltLength),
+ regular_salt.size());
+ EXPECT_NE(incognito_salt, regular_salt);
+}
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