Medium watch ✔︎ verified CVSS 6.5 chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse of released resource in Core
DescriptionUse of released resource in Core
ComponentCore
Sub-componentMedia
Bug ClassLogic Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety — revised by independent review by AI
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.
Independent review by AIConfirmed (tier revised from patch-now)
Reviewer notes: Primitive inflated to 'corruption': diff shows only a lifetime/scoping fix (MakeSelfOwnedReceiver->DocumentService), no memory corruption. Object outlived its document; URLLoaderFactory is refcounted. Tests assert only remote disconnection.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker497204165
Fix commit38533ef6d722 (chromium/src) +114/-16
CWECWE-672
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-03-28 (per vendor advisory)
Fix landed2026-08-25 (public git)
Disclosed2026-09-22
Patch gap28 days — fix public in source before the advisory

Background

`ProvisionFetcherImpl`
A browser-process media::mojom::ProvisionFetcher implementation 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 RenderFrameHost destruction).
`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.

Key insight
The single mistake was binding 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

  1. Trigger provisioning fetch A renderer using Encrypted Media Extensions requests the media::mojom::ProvisionFetcher interface through FrameInterfaceFactory, causing FrameInterfaceFactoryImpl::CreateProvisionFetcher to create a self-owned ProvisionFetcherImpl.
  2. Detach lifetime from document Because the object is bound via mojo::MakeSelfOwnedReceiver, its lifetime depends only on the message pipe, not on the RenderFrameHost or document that supplied the SharedURLLoaderFactory.
  3. Release the document The frame performs a cross-document navigation or the RenderFrameHost is destroyed, releasing the document context while the renderer keeps its mojo::Remote<ProvisionFetcher> open. (inferred)
  4. Use released resource The renderer calls Retrieve on the stale ProvisionFetcherImpl, exercising it after the associated document has been released. (inferred)

Impact Assessment

An attacker controlling a renderer can keep a 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

FunctionChangeNotes
RenderFrameHost
content/public/browser/provision_fetcher_impl.h
modified
CONTENT_EXPORT
content/public/browser/provision_fetcher_impl.h
modified
ProvisionFetcherImplTest
content/browser/media/provision_fetcher_impl_unittest.cc
modified
TEST_F
content/browser/media/provision_fetcher_impl_unittest.cc
modified

Files Changed

  • content/public/browser/provision_fetcher_impl.h
  • content/browser/media/media_interface_proxy.cc
  • content/public/browser/provision_fetcher_impl.cc
  • content/browser/media/provision_fetcher_impl_unittest.cc
  • content/test/BUILD.gn

Audit Directions

  • `MakeSelfOwnedReceiver` from frame factories
    Audit every interface vended by FrameInterfaceFactory / RenderFrameHost that is bound with mojo::MakeSelfOwnedReceiver instead of DocumentService, since these can outlive their document.
  • Document-scoped resource capture
    Flag 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 interfaces
    Review other media-related browser interfaces created per-frame for the same pipe-only lifetime pattern, confirming teardown on cross-document navigation and RenderFrameHost destruction.
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",
Loading diff…

Regression Test / PoC

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

Original Bug Report

reported by [email protected]

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.cc
  • content/browser/media/url_provision_fetcher.cc
  • content/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:

  1. Server-Side Request Forgery (SSRF) and Sandbox Escape: The fetcher uses GetURLLoaderFactoryForBrowserProcess(). This factory is configured with is_trusted=true and is_orb_enabled=false, effectively bypassing Cross-Origin Resource Sharing (CORS) and Opaque Response Blocking (ORB). URLProvisionFetcher::Retrieve accepts an arbitrary GURL directly from the IPC call and assigns it to the network::ResourceRequest without 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.

  2. Lifetime Escape (Persistence): ProvisionFetcherImpl::Create binds the implementation using mojo::MakeSelfOwnedReceiver. This ties the object’s lifetime strictly to the Mojo pipe rather than the RenderFrameHost or 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.

  3. 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.

  1. 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).
  2. Interface Access: The compromised process invokes CreateProvisionFetcher on its legitimately obtained media::mojom::FrameInterfaceFactory Mojo remote.
  3. Persistent Binding: The browser process binds the request using mojo::MakeSelfOwnedReceiver and a privileged URLLoaderFactory (GetURLLoaderFactoryForBrowserProcess()). The attacker retains this Mojo pipe indefinitely.
  4. SSRF Payload: The attacker sends a Retrieve(default_url, request_data) IPC message, providing a malicious default_url (e.g., an internal metadata server or intranet endpoint).
  5. 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.
  6. 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.
  1. Document Lifetime Tie: Replace mojo::MakeSelfOwnedReceiver with content::DocumentService (or similar document-scoped binding) to ensure the ProvisionFetcherImpl’s lifetime is securely tied to the RenderFrameHost that created it.
  2. URL Validation: Implement strict validation or whitelisting for the default_url parameter in URLProvisionFetcher::Retrieve to ensure it only targets legitimate Widevine/CDM provisioning servers.
  3. Bounded Downloads: Replace the use of DownloadToStringOfUnboundedSizeUntilCrashAndDie with a bounded download method (e.g., DownloadToString with 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.

View on issue tracker