CVE-2026-95366
Overview
Background
- `ProvisionFetcherImpl`
- A browser-process
media::mojom::ProvisionFetcherimplementation that fetches DRM provisioning data over the network on behalf of a renderer’s EME/CDM code. - `mojo::MakeSelfOwnedReceiver`
- A Mojo helper that ties an implementation’s lifetime solely to its own message pipe, deleting it only when the pipe disconnects, independent of any document or
RenderFrameHost. - `DocumentService`
- A base class for browser-side Mojo interface implementations whose instance is automatically destroyed when its associated document is torn down (cross-document navigation or
RenderFrameHostdestruction). - `FrameInterfaceFactory`
- The Mojo factory whose documented contract states that every interface it vends is bound to the vending document’s lifetime.
Root Cause Analysis
FrameInterfaceFactoryImpl::CreateProvisionFetcher created a ProvisionFetcherImpl and bound it with mojo::MakeSelfOwnedReceiver, so the object’s lifetime tracked only its own message pipe and not the document that requested it. This violated the FrameInterfaceFactory contract that vended interfaces are scoped to the document, leaving a ProvisionFetcherImpl (built from a SharedURLLoaderFactory derived from the document’s RenderFrameHost / storage partition) alive after the document was destroyed by cross-document navigation or RenderFrameHost teardown. A renderer holding the mojo::Remote<ProvisionFetcher> could then still invoke Retrieve, operating on state tied to the released document context — a use of a released resource.
The fix reparents ProvisionFetcherImpl onto DocumentService<media::mojom::ProvisionFetcher>, passing the RenderFrameHost into Create and the constructor so the instance is destroyed together with its document, exactly like its sibling interfaces. New unit tests assert the mojo::Remote is disconnected after cross-document navigation and after WebContents teardown.
ProvisionFetcherImpl to its own Mojo pipe via mojo::MakeSelfOwnedReceiver instead of to the document’s lifetime, allowing it to outlive the document that owned its resources; the fix makes it a DocumentService, which guarantees teardown with the document.Attack Path
- Trigger provisioning fetch
A renderer using Encrypted Media Extensions requests the
media::mojom::ProvisionFetcherinterface throughFrameInterfaceFactory, causingFrameInterfaceFactoryImpl::CreateProvisionFetcherto create a self-ownedProvisionFetcherImpl. - Detach lifetime from document
Because the object is bound via
mojo::MakeSelfOwnedReceiver, its lifetime depends only on the message pipe, not on theRenderFrameHostor document that supplied theSharedURLLoaderFactory. - Release the document
The frame performs a cross-document navigation or the
RenderFrameHostis destroyed, releasing the document context while the renderer keeps itsmojo::Remote<ProvisionFetcher>open.(inferred) - Use released resource
The renderer calls
Retrieveon the staleProvisionFetcherImpl, exercising it after the associated document has been released.(inferred)
Impact Assessment
ProvisionFetcherImpl alive past its document’s destruction and drive it after teardown, yielding use of a released resource in the browser process (Core component). The precondition is a frame that obtains the EME/CDM provisioning interface and then navigates cross-document or has its RenderFrameHost destroyed while retaining the mojo::Remote. Any escalation to a controlled use-after-free read/write or further compromise is (inferred); the advisory states the impact as “Use of released resource in Core” and reports no exploited-in-the-wild status here.Changed Functions
| Function | Change | Notes |
|---|---|---|
RenderFrameHostcontent/public/browser/provision_fetcher_impl.h |
modified | |
CONTENT_EXPORTcontent/public/browser/provision_fetcher_impl.h |
modified | |
ProvisionFetcherImplTestcontent/browser/media/provision_fetcher_impl_unittest.cc |
modified | |
TEST_Fcontent/browser/media/provision_fetcher_impl_unittest.cc |
modified |
Files Changed
content/public/browser/provision_fetcher_impl.hcontent/browser/media/media_interface_proxy.cccontent/public/browser/provision_fetcher_impl.cccontent/browser/media/provision_fetcher_impl_unittest.cccontent/test/BUILD.gn
Audit Directions
- `MakeSelfOwnedReceiver` from frame factoriesAudit every interface vended by
FrameInterfaceFactory/RenderFrameHostthat is bound withmojo::MakeSelfOwnedReceiverinstead ofDocumentService, since these can outlive their document. - Document-scoped resource captureFlag browser-side Mojo impls that capture
RenderFrameHost-derived state (storage partitions,URLLoaderFactory,BrowserContext) but whose lifetime is not tied to the document that supplied it. - EME/CDM and media interfacesReview other media-related browser interfaces created per-frame for the same pipe-only lifetime pattern, confirming teardown on cross-document navigation and
RenderFrameHostdestruction.
Patch
From 38533ef6d72289716014ca7dd7fc4af5b7c5ec3d Mon Sep 17 00:00:00 2001 From: Sangbaek Park <[email protected]> Date: Tue, 25 Aug 2026 00:13:33 -0700 Subject: [PATCH] media: Make ProvisionFetcherImpl a DocumentService ProvisionFetcherImpl was bound via mojo::MakeSelfOwnedReceiver, so its lifetime depended only on its own message pipe and not on the document for which FrameInterfaceFactoryImpl created it. This violates the contract documented in frame_interface_factory.mojom that interfaces vended by FrameInterfaceFactory are bound to the document's lifetime. Convert ProvisionFetcherImpl to a DocumentService<media::mojom::ProvisionFetcher> so that it is torn down together with the document on cross-document navigation or RenderFrameHost destruction, like its sibling interfaces. Create() now takes the RenderFrameHost; update the sole caller in FrameInterfaceFactoryImpl::CreateProvisionFetcher accordingly. Add provision_fetcher_impl_unittest.cc to content_unittests covering: - ProvisionFetcherImplTest.DestroyedOnCrossDocumentNavigation - ProvisionFetcherImplTest.DestroyedOnWebContentsTeardown These assert that the mojo::Remote<ProvisionFetcher> is disconnected after cross-document navigation and WebContents teardown. Bug: 497204165 Change-Id: Id86c1038bc4c3097ccad2d797442e67dea63cbb0 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8271838 Reviewed-by: Nasko Oskov <[email protected]> Commit-Queue: Sangbaek Park <[email protected]> Reviewed-by: Dale Curtis <[email protected]> Cr-Commit-Position: refs/heads/main@{#1685281} --- diff --git a/content/browser/media/media_interface_proxy.cc b/content/browser/media/media_interface_proxy.cc index 7557b2b..e50d301 100644 --- a/content/browser/media/media_interface_proxy.cc +++ b/content/browser/media/media_interface_proxy.cc @@ -163,7 +163,8 @@ void CreateProvisionFetcher( mojo::PendingReceiver<media::mojom::ProvisionFetcher> receiver) override { #if BUILDFLAG(ENABLE_MOJO_CDM) - ProvisionFetcherImpl::Create(render_frame_host_->GetBrowserContext() + ProvisionFetcherImpl::Create(render_frame_host_, + render_frame_host_->GetBrowserContext() ->GetDefaultStoragePartition() ->GetURLLoaderFactoryForBrowserProcess(), std::move(receiver)); diff --git a/content/public/browser/provision_fetcher_impl.cc b/content/public/browser/provision_fetcher_impl.cc index 57fbb26..f7abfbe7 100644 --- a/content/public/browser/provision_fetcher_impl.cc +++ b/content/public/browser/provision_fetcher_impl.cc @@ -8,29 +8,34 @@ #include "base/functional/bind.h" #include "base/logging.h" #include "content/public/browser/provision_fetcher_factory.h" -#include "mojo/public/cpp/bindings/self_owned_receiver.h" #include "services/network/public/cpp/shared_url_loader_factory.h" namespace content { // static void ProvisionFetcherImpl::Create( + RenderFrameHost* render_frame_host, scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory, mojo::PendingReceiver<media::mojom::ProvisionFetcher> receiver) { + CHECK(render_frame_host); DCHECK(url_loader_factory); - mojo::MakeSelfOwnedReceiver( - std::make_unique<ProvisionFetcherImpl>( - CreateProvisionFetcher(std::move(url_loader_factory))), - std::move(receiver)); + // The created ProvisionFetcherImpl is bound to (and owned by) `receiver` and + // the document associated with `render_frame_host`. + new ProvisionFetcherImpl( + *render_frame_host, std::move(receiver), + CreateProvisionFetcher(std::move(url_loader_factory))); } ProvisionFetcherImpl::ProvisionFetcherImpl( + RenderFrameHost& render_frame_host, + mojo::PendingReceiver<media::mojom::ProvisionFetcher> receiver, std::unique_ptr<media::ProvisionFetcher> provision_fetcher) - : provision_fetcher_(std::move(provision_fetcher)) { + : DocumentService(render_frame_host, std::move(receiver)), + provision_fetcher_(std::move(provision_fetcher)) { DVLOG(1) << __FUNCTION__; } -ProvisionFetcherImpl::~ProvisionFetcherImpl() {} +ProvisionFetcherImpl::~ProvisionFetcherImpl() = default; void ProvisionFetcherImpl::Retrieve(const GURL& default_url, const std::string& request_data, diff --git a/content/public/browser/provision_fetcher_impl.h b/content/public/browser/provision_fetcher_impl.h index 8b39fcb..94121e8 100644 --- a/content/public/browser/provision_fetcher_impl.h +++ b/content/public/browser/provision_fetcher_impl.h @@ -9,7 +9,7 @@ #include "base/memory/weak_ptr.h" #include "content/common/content_export.h" -#include "content/public/browser/provision_fetcher_factory.h" +#include "content/public/browser/document_service.h" #include "media/base/provision_fetcher.h" #include "media/mojo/mojom/provision_fetcher.mojom.h" #include "mojo/public/cpp/bindings/pending_receiver.h" @@ -20,29 +20,33 @@ namespace content { +class RenderFrameHost; + // A media::mojom::ProvisionFetcher implementation based on // media::ProvisionFetcher. -class CONTENT_EXPORT ProvisionFetcherImpl - : public media::mojom::ProvisionFetcher { +class CONTENT_EXPORT ProvisionFetcherImpl final + : public DocumentService<media::mojom::ProvisionFetcher> { public: static void Create( + RenderFrameHost* render_frame_host, scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory, mojo::PendingReceiver<media::mojom::ProvisionFetcher> receiver); - explicit ProvisionFetcherImpl( - std::unique_ptr<media::ProvisionFetcher> provision_fetcher); - ProvisionFetcherImpl(const ProvisionFetcherImpl&) = delete; ProvisionFetcherImpl& operator=(const ProvisionFetcherImpl&) = delete; - ~ProvisionFetcherImpl() override; - // media::mojom::ProvisionFetcher implementation. void Retrieve(const GURL& default_url, const std::string& request_data, RetrieveCallback callback) final; private: + ProvisionFetcherImpl( + RenderFrameHost& render_frame_host, + mojo::PendingReceiver<media::mojom::ProvisionFetcher> receiver, + std::unique_ptr<media::ProvisionFetcher> provision_fetcher); + ~ProvisionFetcherImpl() override; + // Callback for media::ProvisionFetcher::Retrieve(). void OnResponse(RetrieveCallback callback, bool success, diff --git a/content/browser/media/provision_fetcher_impl_unittest.cc b/content/browser/media/provision_fetcher_impl_unittest.cc new file mode 100644 index 0000000..f0e8a97 --- /dev/null +++ b/content/browser/media/provision_fetcher_impl_unittest.cc @@ -0,0 +1,87 @@ +// 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 "content/public/browser/provision_fetcher_impl.h" + +#include "base/run_loop.h" +#include "content/public/browser/back_forward_cache.h" +#include "content/public/browser/navigation_controller.h" +#include "content/public/browser/web_contents.h" +#include "content/public/test/navigation_simulator.h" +#include "content/public/test/test_renderer_host.h" +#include "media/mojo/mojom/provision_fetcher.mojom.h" +#include "mojo/public/cpp/bindings/remote.h" +#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h" +#include "services/network/test/test_url_loader_factory.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +namespace content { + +namespace { + +constexpr char kFirstUrl[] = "https://a.com/"; +constexpr char kSecondUrl[] = "https://b.com/"; + +RenderFrameHost* SimulateNavigation(RenderFrameHost* rfh, const GURL& url) { + auto navigation_simulator = + NavigationSimulator::CreateRendererInitiated(url, rfh); + navigation_simulator->Commit(); + return navigation_simulator->GetFinalRenderFrameHost(); +} + +} // namespace + +class ProvisionFetcherImplTest : public RenderViewHostTestHarness { + protected: + void SetUp() override { + RenderViewHostTestHarness::SetUp(); + shared_url_loader_factory_ = + base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>( + &test_url_loader_factory_); + RenderFrameHostTester::For(main_rfh())->InitializeRenderFrameIfNeeded(); + SimulateNavigation(main_rfh(), GURL(kFirstUrl)); + } + + void TearDown() override { + shared_url_loader_factory_.reset(); + RenderViewHostTestHarness::TearDown(); + } + + network::TestURLLoaderFactory test_url_loader_factory_; + scoped_refptr<network::SharedURLLoaderFactory> shared_url_loader_factory_; +}; + +TEST_F(ProvisionFetcherImplTest, DestroyedOnCrossDocumentNavigation) { + web_contents()->GetController().GetBackForwardCache().DisableForTesting( + BackForwardCache::TEST_REQUIRES_NO_CACHING); + + mojo::Remote<media::mojom::ProvisionFetcher> remote; + ProvisionFetcherImpl::Create(main_rfh(), shared_url_loader_factory_, + remote.BindNewPipeAndPassReceiver()); + remote.FlushForTesting(); + ASSERT_TRUE(remote.is_connected()); + + SimulateNavigation(main_rfh(), GURL(kSecondUrl)); + base::RunLoop().RunUntilIdle(); + remote.FlushForTesting(); + + EXPECT_FALSE(remote.is_connected()); +} + +TEST_F(ProvisionFetcherImplTest, DestroyedOnWebContentsTeardown) { + mojo::Remote<media::mojom::ProvisionFetcher> remote; + ProvisionFetcherImpl::Create(main_rfh(), shared_url_loader_factory_, + remote.BindNewPipeAndPassReceiver()); + remote.FlushForTesting(); + ASSERT_TRUE(remote.is_connected()); + + DeleteContents(); + base::RunLoop().RunUntilIdle(); + remote.FlushForTesting(); + + EXPECT_FALSE(remote.is_connected()); +} + +} // namespace content diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn index ec2ca2b..3d480146 100644 --- a/content/test/BUILD.gn +++ b/content/test/BUILD.gn @@ -2663,6 +2663,7 @@ "../browser/media/media_power_experiment_manager_unittest.cc", "../browser/media/media_web_contents_observer_unittest.cc", "../browser/media/midi_host_unittest.cc", + "../browser/media/provision_fetcher_impl_unittest.cc", "../browser/media/session/media_players_callback_aggregator_unittest.cc", "../browser/media/session/media_session_controller_unittest.cc", "../browser/media/session/media_session_controllers_manager_unittest.cc",
Regression Test / PoC
diff --git a/content/browser/media/provision_fetcher_impl_unittest.cc b/content/browser/media/provision_fetcher_impl_unittest.cc
new file mode 100644
index 0000000..f0e8a97
--- /dev/null
+++ b/content/browser/media/provision_fetcher_impl_unittest.cc
@@ -0,0 +1,87 @@
+// 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 "content/public/browser/provision_fetcher_impl.h"
+
+#include "base/run_loop.h"
+#include "content/public/browser/back_forward_cache.h"
+#include "content/public/browser/navigation_controller.h"
+#include "content/public/browser/web_contents.h"
+#include "content/public/test/navigation_simulator.h"
+#include "content/public/test/test_renderer_host.h"
+#include "media/mojo/mojom/provision_fetcher.mojom.h"
+#include "mojo/public/cpp/bindings/remote.h"
+#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
+#include "services/network/test/test_url_loader_factory.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
+
+namespace content {
+
+namespace {
+
+constexpr char kFirstUrl[] = "https://a.com/";
+constexpr char kSecondUrl[] = "https://b.com/";
+
+RenderFrameHost* SimulateNavigation(RenderFrameHost* rfh, const GURL& url) {
+ auto navigation_simulator =
+ NavigationSimulator::CreateRendererInitiated(url, rfh);
+ navigation_simulator->Commit();
+ return navigation_simulator->GetFinalRenderFrameHost();
+}
+
+} // namespace
+
+class ProvisionFetcherImplTest : public RenderViewHostTestHarness {
+ protected:
+ void SetUp() override {
+ RenderViewHostTestHarness::SetUp();
+ shared_url_loader_factory_ =
+ base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
+ &test_url_loader_factory_);
+ RenderFrameHostTester::For(main_rfh())->InitializeRenderFrameIfNeeded();
+ SimulateNavigation(main_rfh(), GURL(kFirstUrl));
+ }
+
+ void TearDown() override {
+ shared_url_loader_factory_.reset();
+ RenderViewHostTestHarness::TearDown();
+ }
+
+ network::TestURLLoaderFactory test_url_loader_factory_;
+ scoped_refptr<network::SharedURLLoaderFactory> shared_url_loader_factory_;
+};
+
+TEST_F(ProvisionFetcherImplTest, DestroyedOnCrossDocumentNavigation) {
+ web_contents()->GetController().GetBackForwardCache().DisableForTesting(
+ BackForwardCache::TEST_REQUIRES_NO_CACHING);
+
+ mojo::Remote<media::mojom::ProvisionFetcher> remote;
+ ProvisionFetcherImpl::Create(main_rfh(), shared_url_loader_factory_,
+ remote.BindNewPipeAndPassReceiver());
+ remote.FlushForTesting();
+ ASSERT_TRUE(remote.is_connected());
+
+ SimulateNavigation(main_rfh(), GURL(kSecondUrl));
+ base::RunLoop().RunUntilIdle();
+ remote.FlushForTesting();
+
+ EXPECT_FALSE(remote.is_connected());
+}
+
+TEST_F(ProvisionFetcherImplTest, DestroyedOnWebContentsTeardown) {
+ mojo::Remote<media::mojom::ProvisionFetcher> remote;
+ ProvisionFetcherImpl::Create(main_rfh(), shared_url_loader_factory_,
+ remote.BindNewPipeAndPassReceiver());
+ remote.FlushForTesting();
+ ASSERT_TRUE(remote.is_connected());
+
+ DeleteContents();
+ base::RunLoop().RunUntilIdle();
+ remote.FlushForTesting();
+
+ EXPECT_FALSE(remote.is_connected());
+}
+
+} // namespace content
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index ec2ca2b..3d480146 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -2663,6 +2663,7 @@
"../browser/media/media_power_experiment_manager_unittest.cc",
"../browser/media/media_web_contents_observer_unittest.cc",
"../browser/media/midi_host_unittest.cc",
+ "../browser/media/provision_fetcher_impl_unittest.cc",
"../browser/media/session/media_players_callback_aggregator_unittest.cc",
"../browser/media/session/media_session_controller_unittest.cc",
"../browser/media/session/media_session_controllers_manager_unittest.cc",
Original Bug Report
SSRF, Browser DoS, and Lifetime Escape via ProvisionFetcherImpl
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A compromised CDM or GPU process can exploit ProvisionFetcherImpl to perform arbitrary POST requests using a privileged URLLoaderFactory that bypasses CORS and ORB. The implementation allows persistent connections that outlive the document, and unbounded downloads can trigger a reliable browser-process Out-Of-Memory (OOM) crash.
Affected files:
content/public/browser/provision_fetcher_impl.cccontent/browser/media/url_provision_fetcher.cccontent/browser/media/media_interface_proxy.cc
Estimated timestamp from git blame: 2026-03-06
Summary
ProvisionFetcherImpl (in content/public/browser/provision_fetcher_impl.cc) is exposed to less-privileged child processes (like the GPU process on Android or the CDM utility process on Desktop) via FrameInterfaceFactory::CreateProvisionFetcher. This interface allows the child process to perform arbitrary network requests to an attacker-controlled URL without validation, utilizing a highly privileged browser-process URLLoaderFactory.
This vulnerability has three key components that a compromised child process could combine for significant impact:
-
Server-Side Request Forgery (SSRF) and Sandbox Escape: The fetcher uses
GetURLLoaderFactoryForBrowserProcess(). This factory is configured withis_trusted=trueandis_orb_enabled=false, effectively bypassing Cross-Origin Resource Sharing (CORS) and Opaque Response Blocking (ORB).URLProvisionFetcher::Retrieveaccepts an arbitraryGURLdirectly from the IPC call and assigns it to thenetwork::ResourceRequestwithout validation. This allows a compromised process to make uncredentialed POST requests to arbitrary URLs (including localhost and intranet addresses) and read the full response body. -
Lifetime Escape (Persistence):
ProvisionFetcherImpl::Createbinds the implementation usingmojo::MakeSelfOwnedReceiver. This ties the object’s lifetime strictly to the Mojo pipe rather than theRenderFrameHostor document that created it. An attacker can maintain this connection persistently, retaining the ability to perform SSRF even after the user navigates away from the page that initially requested the CDM. -
Browser-Process Denial of Service (DoS): The fetcher downloads the response using
DownloadToStringOfUnboundedSizeUntilCrashAndDie. An attacker pointing the fetcher to a server returning an infinite stream of data can cause the browser process to buffer indefinitely, leading to a reliable Out-Of-Memory (OOM) crash and full browser Denial of Service.
Potential Exploitation Steps
Note: These steps are based on static analysis; our tooling agent does not currently have the ability to run a live exploit.
- Initial Compromise: The attacker exploits a vulnerability in a sandboxed process hosting the Content Decryption Module (CDM), such as the GPU process (Android) or the CDM utility process (Desktop).
- Interface Access: The compromised process invokes
CreateProvisionFetcheron its legitimately obtainedmedia::mojom::FrameInterfaceFactoryMojo remote. - Persistent Binding: The browser process binds the request using
mojo::MakeSelfOwnedReceiverand a privilegedURLLoaderFactory(GetURLLoaderFactoryForBrowserProcess()). The attacker retains this Mojo pipe indefinitely. - SSRF Payload: The attacker sends a
Retrieve(default_url, request_data)IPC message, providing a maliciousdefault_url(e.g., an internal metadata server or intranet endpoint). - Execution and Exfiltration: The browser process executes the POST request (bypassing CORS/ORB due to the privileged factory) and returns the full response body to the attacker via the Mojo callback.
- DoS Trigger: Alternatively, the attacker provides a URL returning an infinite data stream. The browser process attempts to buffer this via
DownloadToStringOfUnboundedSizeUntilCrashAndDie, leading to an OOM crash.
Recommended Fix
- Document Lifetime Tie: Replace
mojo::MakeSelfOwnedReceiverwithcontent::DocumentService(or similar document-scoped binding) to ensure theProvisionFetcherImpl’s lifetime is securely tied to theRenderFrameHostthat created it. - URL Validation: Implement strict validation or whitelisting for the
default_urlparameter inURLProvisionFetcher::Retrieveto ensure it only targets legitimate Widevine/CDM provisioning servers. - Bounded Downloads: Replace the use of
DownloadToStringOfUnboundedSizeUntilCrashAndDiewith a bounded download method (e.g.,DownloadToStringwith a reasonable maximum size) to prevent memory exhaustion and DoS.
Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0
Results from 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. And please feel free to reach out to me directly if you have concerns or feedback on the project.