Chrome · Media › Media
CVE-2026-106355
Authorization Error in Media (Media)
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/media/media_interface_proxy.cc |
modified | |
MediaInterfaceProxyTestcontent/browser/media/media_interface_proxy_unittest.cc |
modified | |
MediaInterfaceProxyOffTheRecordTestcontent/browser/media/media_interface_proxy_unittest.cc |
modified | |
TEST_Fcontent/browser/media/media_interface_proxy_unittest.cc |
modified |
Files Changed
content/browser/media/media_interface_proxy_unittest.cccontent/browser/media/media_interface_proxy.cccontent/test/BUILD.gn
Patch
From b1b82589b747cdd79dfa8e583ce1518aab592b9f Mon Sep 17 00:00:00 2001
From: Sangbaek Park <sangbaekpark@chromium.org>
Date: Mon, 14 Sep 2026 11:59:54 -0700
Subject: [PATCH] media: Disallow hardware-secure CDM off the record
On Windows, the browser-side MediaFoundation CDM plumbing lacked an
off-the-record check: the only Incognito enforcement was renderer-side
key-system filtering. A compromised renderer can bypass renderer-side
filtering and directly invoke media::mojom::InterfaceFactory::CreateCdm
on MediaInterfaceProxy in the browser process.
When this occurred in an off-the-record profile, the browser launched
the out-of-process MediaFoundationService utility process pointing to
the regular profile's MediaFoundationCdmStore directory. This leaked
the regular profile's distinctive identifier (origin_id) and client
token, and persisted DRM state to the regular profile directory,
violating Incognito privacy guarantees.
This CL adds an IsOffTheRecord() check in MediaInterfaceProxy::CreateCdm
for hardware-secure MediaFoundation CDMs. If the hosting BrowserContext
is off-the-record, CDM creation is rejected with kCdmNotSupported.
Bug: 559777100
Change-Id: I50d435073dfdfabbb37694aac0a3f09e593140d4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8395341
Reviewed-by: Dale Curtis <dalecurtis@chromium.org>
Commit-Queue: Sangbaek Park <sangbaekpark@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1697400}
---
diff --git a/content/browser/media/media_interface_proxy.cc b/content/browser/media/media_interface_proxy.cc
index e03f4c6..6417168 100644
--- a/content/browser/media/media_interface_proxy.cc
+++ b/content/browser/media/media_interface_proxy.cc
@@ -24,6 +24,7 @@
#include "content/browser/media/media_web_contents_observer.h"
#include "content/browser/renderer_host/render_frame_host_delegate.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
+#include "content/public/browser/browser_context.h"
#include "content/public/browser/media_service.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/security_principal.h"
@@ -43,7 +44,6 @@
#include "mojo/public/cpp/bindings/remote_set.h"
#if BUILDFLAG(ENABLE_CDM_PROVISION_FETCHER)
-#include "content/public/browser/browser_context.h"
#include "content/public/browser/provision_fetcher_impl.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/storage_partition.h"
@@ -559,6 +559,25 @@
auto cdm_info = CdmRegistryImpl::GetInstance()->GetCdmInfo(
cdm_config.key_system, CdmInfo::Robustness::kHardwareSecure);
if (cdm_info) {
+ // The MediaFoundation CDM path has no off-the-record dimension:
+ // CdmDocumentServiceImpl::GetMediaFoundationCdmData reads the REGULAR
+ // profile's persistent per-origin CDM identity (origin_id /
+ // client_token in prefs::kMediaCdmOriginData) through the incognito
+ // pref overlay, and pre-creates persistent CDM storage under the
+ // REGULAR profile's directory (OffTheRecordProfileImpl::GetPath()
+ // aliases it), which the OS CDM then uses as its durable store —
+ // surviving incognito teardown. Renderer-side incognito key-system
+ // filtering (e.g. AddPlayReady's can_persist_data check) is not a
+ // security boundary, so enforce this browser-side: refuse to launch
+ // the MediaFoundation CDM for off-the-record contexts.
+ if (render_frame_host().GetBrowserContext()->IsOffTheRecord()) {
+ DVLOG(2) << "MediaFoundation CDM not supported off the record";
+ std::move(create_cdm_cb)
+ .Run(mojo::NullRemote(), nullptr,
+ media::CreateCdmStatus::kCdmNotSupported);
+ return;
+ }
+
DVLOG(2) << "Get MediaFoundationService with CDM path " << cdm_info->path;
bool is_cached_factory = mf_interface_factory_remote_.is_bound();
auto* factory = GetMediaFoundationServiceInterfaceFactory(cdm_info->path);
diff --git a/content/browser/media/media_interface_proxy_unittest.cc b/content/browser/media/media_interface_proxy_unittest.cc
new file mode 100644
index 0000000..f70e58d
--- /dev/null
+++ b/content/browser/media/media_interface_proxy_unittest.cc
@@ -0,0 +1,140 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <memory>
+#include <optional>
+#include <string>
+
+#include "base/command_line.h"
+#include "base/files/file_path.h"
+#include "base/run_loop.h"
+#include "base/test/mock_callback.h"
+#include "build/build_config.h"
+#include "content/browser/media/cdm_registry_impl.h"
+#include "content/browser/renderer_host/render_frame_host_impl.h"
+#include "content/common/pseudonymization_salt.h"
+#include "content/public/common/cdm_info.h"
+#include "content/public/test/test_browser_context.h"
+#include "content/public/test/test_renderer_host.h"
+#include "media/base/cdm_capability.h"
+#include "media/base/cdm_config.h"
+#include "media/mojo/mojom/content_decryption_module.mojom.h"
+#include "media/mojo/mojom/interface_factory.mojom.h"
+#include "mojo/public/cpp/bindings/pending_remote.h"
+#include "mojo/public/cpp/bindings/remote.h"
+#include "sandbox/policy/switches.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace content {
+
+namespace {
+
+const char kTestKeySystem[] = "org.chromium.externalclearkey.mediafoundation";
+
+} // namespace
+
+class MediaInterfaceProxyTest : public RenderViewHostTestHarness {
+ public:
+ void SetUp() override {
+ RenderViewHostTestHarness::SetUp();
+ SetPseudonymizationSalt(12345);
+ base::CommandLine::ForCurrentProcess()->AppendSwitch(
+ sandbox::policy::switches::kNoSandbox);
+
+ // Register a hardware-secure CDM in CdmRegistry.
+ CdmRegistryImpl::GetInstance()->RegisterCdm(CdmInfo(
+ kTestKeySystem, CdmInfo::Robustness::kHardwareSecure, std::nullopt,
+ /*supports_sub_key_systems=*/false, "Test Hardware Secure CDM",
+ media::CdmType(), base::FilePath(FILE_PATH_LITERAL("test_cdm_path"))));
+ }
+
+ void TearDown() override {
+ ResetSaltForTesting();
+ RenderViewHostTestHarness::TearDown();
+ }
+
+ mojo::Remote<media::mojom::InterfaceFactory> GetMediaInterfaceFactory() {
+ mojo::Remote<media::mojom::InterfaceFactory> factory;
+ static_cast<RenderFrameHostImpl*>(main_rfh())
+ ->BindMediaInterfaceFactoryReceiver(
+ factory.BindNewPipeAndPassReceiver());
+ return factory;
+ }
+
+ media::CdmConfig CreateHwSecureCdmConfig() {
+ media::CdmConfig config;
+ config.key_system = kTestKeySystem;
+ config.allow_distinctive_identifier = true;
+ config.allow_persistent_state = true;
+ config.use_hw_secure_codecs = true;
+ return config;
+ }
+};
+
+class MediaInterfaceProxyOffTheRecordTest : public MediaInterfaceProxyTest {
+ protected:
+ std::unique_ptr<BrowserContext> CreateBrowserContext() override {
+ auto browser_context = std::make_unique<TestBrowserContext>();
+ browser_context->set_is_off_the_record(true);
+ return browser_context;
+ }
+};
+
+#if BUILDFLAG(IS_WIN)
+TEST_F(MediaInterfaceProxyOffTheRecordTest,
+ CreateCdm_MediaFoundation_OffTheRecord) {
+ auto factory = GetMediaInterfaceFactory();
+ ASSERT_TRUE(factory.is_bound());
+
+ base::MockCallback<media::mojom::InterfaceFactory::CreateCdmCallback> mock_cb;
+ std::optional<media::CreateCdmStatus> created_status;
+ base::RunLoop run_loop;
+
+ EXPECT_CALL(mock_cb, Run(testing::_, testing::_, testing::_))
+ .WillOnce([&](mojo::PendingRemote<media::mojom::ContentDecryptionModule>,
+ media::mojom::CdmContextPtr,
+ media::CreateCdmStatus status) {
+ created_status = status;
+ run_loop.Quit();
+ });
+
+ factory->CreateCdm(CreateHwSecureCdmConfig(), mock_cb.Get());
+ run_loop.Run();
+
+ // In off-the-record contexts, hardware-secure MediaFoundation CDM creation
+ // must be refused to prevent leaking regular profile DRM identity and writing
+ // persistent storage to the regular profile directory.
+ ASSERT_TRUE(created_status.has_value());
+ EXPECT_EQ(created_status.value(), media::CreateCdmStatus::kCdmNotSupported);
+}
+
+TEST_F(MediaInterfaceProxyTest, CreateCdm_MediaFoundation_InvalidConfig) {
+ auto factory = GetMediaInterfaceFactory();
+ ASSERT_TRUE(factory.is_bound());
+
+ media::CdmConfig config = CreateHwSecureCdmConfig();
+ config.allow_persistent_state = false;
+
+ base::MockCallback<media::mojom::InterfaceFactory::CreateCdmCallback> mock_cb;
+ std::optional<media::CreateCdmStatus> created_status;
+ base::RunLoop run_loop;
+
+ EXPECT_CALL(mock_cb, Run(testing::_, testing::_, testing::_))
+ .WillOnce([&](mojo::PendingRemote<media::mojom::ContentDecryptionModule>,
+ media::mojom::CdmContextPtr,
+ media::CreateCdmStatus status) {
+ created_status = status;
+ run_loop.Quit();
+ });
+
+ factory->CreateCdm(config, mock_cb.Get());
+ run_loop.Run();
+
+ ASSERT_TRUE(created_status.has_value());
+ EXPECT_EQ(created_status.value(), media::CreateCdmStatus::kInvalidCdmConfig);
+}
+#endif // BUILDFLAG(IS_WIN)
+
+} // namespace content
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index e843d9d..830f841 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -3503,6 +3503,7 @@
"../browser/child_process_sandbox_support_win_unittest.cc",
"../browser/device_posture/device_posture_registry_watcher_win_unittest.cc",
"../browser/haptics/haptics_manager_impl_win_unittest.cc",
+ "../browser/media/media_interface_proxy_unittest.cc",
"../browser/renderer_host/direct_manipulation_test_helper_win.cc",
"../browser/renderer_host/direct_manipulation_test_helper_win.h",
"../browser/renderer_host/direct_manipulation_win_unittest.cc",
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/content/browser/media/media_interface_proxy_unittest.cc b/content/browser/media/media_interface_proxy_unittest.cc
new file mode 100644
index 0000000..f70e58d
--- /dev/null
+++ b/content/browser/media/media_interface_proxy_unittest.cc
@@ -0,0 +1,140 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <memory>
+#include <optional>
+#include <string>
+
+#include "base/command_line.h"
+#include "base/files/file_path.h"
+#include "base/run_loop.h"
+#include "base/test/mock_callback.h"
+#include "build/build_config.h"
+#include "content/browser/media/cdm_registry_impl.h"
+#include "content/browser/renderer_host/render_frame_host_impl.h"
+#include "content/common/pseudonymization_salt.h"
+#include "content/public/common/cdm_info.h"
+#include "content/public/test/test_browser_context.h"
+#include "content/public/test/test_renderer_host.h"
+#include "media/base/cdm_capability.h"
+#include "media/base/cdm_config.h"
+#include "media/mojo/mojom/content_decryption_module.mojom.h"
+#include "media/mojo/mojom/interface_factory.mojom.h"
+#include "mojo/public/cpp/bindings/pending_remote.h"
+#include "mojo/public/cpp/bindings/remote.h"
+#include "sandbox/policy/switches.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace content {
+
+namespace {
+
+const char kTestKeySystem[] = "org.chromium.externalclearkey.mediafoundation";
+
+} // namespace
+
+class MediaInterfaceProxyTest : public RenderViewHostTestHarness {
+ public:
+ void SetUp() override {
+ RenderViewHostTestHarness::SetUp();
+ SetPseudonymizationSalt(12345);
+ base::CommandLine::ForCurrentProcess()->AppendSwitch(
+ sandbox::policy::switches::kNoSandbox);
+
+ // Register a hardware-secure CDM in CdmRegistry.
+ CdmRegistryImpl::GetInstance()->RegisterCdm(CdmInfo(
+ kTestKeySystem, CdmInfo::Robustness::kHardwareSecure, std::nullopt,
+ /*supports_sub_key_systems=*/false, "Test Hardware Secure CDM",
+ media::CdmType(), base::FilePath(FILE_PATH_LITERAL("test_cdm_path"))));
+ }
+
+ void TearDown() override {
+ ResetSaltForTesting();
+ RenderViewHostTestHarness::TearDown();
+ }
+
+ mojo::Remote<media::mojom::InterfaceFactory> GetMediaInterfaceFactory() {
+ mojo::Remote<media::mojom::InterfaceFactory> factory;
+ static_cast<RenderFrameHostImpl*>(main_rfh())
+ ->BindMediaInterfaceFactoryReceiver(
+ factory.BindNewPipeAndPassReceiver());
+ return factory;
+ }
+
+ media::CdmConfig CreateHwSecureCdmConfig() {
+ media::CdmConfig config;
+ config.key_system = kTestKeySystem;
+ config.allow_distinctive_identifier = true;
+ config.allow_persistent_state = true;
+ config.use_hw_secure_codecs = true;
+ return config;
+ }
+};
+
+class MediaInterfaceProxyOffTheRecordTest : public MediaInterfaceProxyTest {
+ protected:
+ std::unique_ptr<BrowserContext> CreateBrowserContext() override {
+ auto browser_context = std::make_unique<TestBrowserContext>();
+ browser_context->set_is_off_the_record(true);
+ return browser_context;
+ }
+};
+
+#if BUILDFLAG(IS_WIN)
+TEST_F(MediaInterfaceProxyOffTheRecordTest,
+ CreateCdm_MediaFoundation_OffTheRecord) {
+ auto factory = GetMediaInterfaceFactory();
+ ASSERT_TRUE(factory.is_bound());
+
+ base::MockCallback<media::mojom::InterfaceFactory::CreateCdmCallback> mock_cb;
+ std::optional<media::CreateCdmStatus> created_status;
+ base::RunLoop run_loop;
+
+ EXPECT_CALL(mock_cb, Run(testing::_, testing::_, testing::_))
+ .WillOnce([&](mojo::PendingRemote<media::mojom::ContentDecryptionModule>,
+ media::mojom::CdmContextPtr,
+ media::CreateCdmStatus status) {
+ created_status = status;
+ run_loop.Quit();
+ });
+
+ factory->CreateCdm(CreateHwSecureCdmConfig(), mock_cb.Get());
+ run_loop.Run();
+
+ // In off-the-record contexts, hardware-secure MediaFoundation CDM creation
+ // must be refused to prevent leaking regular profile DRM identity and writing
+ // persistent storage to the regular profile directory.
+ ASSERT_TRUE(created_status.has_value());
+ EXPECT_EQ(created_status.value(), media::CreateCdmStatus::kCdmNotSupported);
+}
+
+TEST_F(MediaInterfaceProxyTest, CreateCdm_MediaFoundation_InvalidConfig) {
+ auto factory = GetMediaInterfaceFactory();
+ ASSERT_TRUE(factory.is_bound());
+
+ media::CdmConfig config = CreateHwSecureCdmConfig();
+ config.allow_persistent_state = false;
+
+ base::MockCallback<media::mojom::InterfaceFactory::CreateCdmCallback> mock_cb;
+ std::optional<media::CreateCdmStatus> created_status;
+ base::RunLoop run_loop;
+
+ EXPECT_CALL(mock_cb, Run(testing::_, testing::_, testing::_))
+ .WillOnce([&](mojo::PendingRemote<media::mojom::ContentDecryptionModule>,
+ media::mojom::CdmContextPtr,
+ media::CreateCdmStatus status) {
+ created_status = status;
+ run_loop.Quit();
+ });
+
+ factory->CreateCdm(config, mock_cb.Get());
+ run_loop.Run();
+
+ ASSERT_TRUE(created_status.has_value());
+ EXPECT_EQ(created_status.value(), media::CreateCdmStatus::kInvalidCdmConfig);
+}
+#endif // BUILDFLAG(IS_WIN)
+
+} // namespace content
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index e843d9d..830f841 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -3503,6 +3503,7 @@
"../browser/child_process_sandbox_support_win_unittest.cc",
"../browser/device_posture/device_posture_registry_watcher_win_unittest.cc",
"../browser/haptics/haptics_manager_impl_win_unittest.cc",
+ "../browser/media/media_interface_proxy_unittest.cc",
"../browser/renderer_host/direct_manipulation_test_helper_win.cc",
"../browser/renderer_host/direct_manipulation_test_helper_win.h",
"../browser/renderer_host/direct_manipulation_win_unittest.cc",
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