CVE-2026-7979
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
MEDIA_EXPORTmedia/base/data_source.h |
modified | |
MEDIA_EXPORTmedia/filters/hls_data_source_provider_impl.h |
modified | |
DataSourceFactorymedia/filters/hls_data_source_provider_impl.h |
modified | |
ifmedia/filters/hls_test_helpers.cc |
modified | |
MockDataSourceFactorymedia/filters/hls_test_helpers.h |
modified | |
TestDataSourceFactorymedia/test/pipeline_integration_test_base.cc |
modified | |
ifthird_party/blink/renderer/platform/media/BUILD.gn |
modified | |
source_setthird_party/blink/renderer/platform/media/BUILD.gn |
modified |
Files Changed
media/base/data_source.ccmedia/base/data_source.hmedia/filters/hls_data_source_provider_impl.ccmedia/filters/hls_data_source_provider_impl.hmedia/filters/hls_test_helpers.ccmedia/filters/hls_test_helpers.hmedia/test/pipeline_integration_test_base.ccthird_party/blink/renderer/platform/media/BUILD.gn
Patch
From 75bbf79378ca5b5c64d95c6f91018ea126f6eeac Mon Sep 17 00:00:00 2001 From: Ted Meyer <[email protected]> Date: Fri, 03 Apr 2026 18:18:10 -0700 Subject: [PATCH] Relocate HlsDataSourceProviderImpl::DataSourceFactory to new home There was really no reason for this to exist so tightly coupled to the HLS code, and it's perfectly workable as a subclass of DataSource itself. It'll also allow a mixed data source factory in the future, allowing data:// urls and actual remote urls to be accessed from the same meta-factory, but that comes later. Bug: 497849876 Change-Id: Id4f4f65d4347715ff45101edc4aa28dd7ab9a03a Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7712714 Commit-Queue: Ted (Chromium) Meyer <[email protected]> Reviewed-by: Eugene Zemtsov <[email protected]> Cr-Commit-Position: refs/heads/main@{#1610061} --- diff --git a/media/base/data_source.cc b/media/base/data_source.cc index 2197cf3..d56753ba 100644 --- a/media/base/data_source.cc +++ b/media/base/data_source.cc @@ -13,6 +13,8 @@ DataSource::~DataSource() = default; +DataSource::Factory::~Factory() = default; + bool DataSource::AssumeFullyBuffered() const { return true; } diff --git a/media/base/data_source.h b/media/base/data_source.h index 70107724..31089d2 100644 --- a/media/base/data_source.h +++ b/media/base/data_source.h @@ -38,6 +38,7 @@ class MEDIA_EXPORT DataSource : public DataSourceInfo { public: using ReadCB = base::OnceCallback<void(int)>; + using DataSourceCb = base::OnceCallback<void(std::unique_ptr<DataSource>)>; enum { kReadError = -1, kAborted = -2 }; @@ -54,6 +55,14 @@ AUTO, }; + class MEDIA_EXPORT Factory { + public: + virtual ~Factory(); + virtual void Create(const GURL& uri, + bool ignore_cache, + DataSourceCb cb) = 0; + }; + DataSource(); DataSource(const DataSource&) = delete; diff --git a/media/filters/hls_data_source_provider_impl.cc b/media/filters/hls_data_source_provider_impl.cc index 864c1b7..bcef619 100644 --- a/media/filters/hls_data_source_provider_impl.cc +++ b/media/filters/hls_data_source_provider_impl.cc @@ -53,7 +53,7 @@ } // namespace HlsDataSourceProviderImpl::HlsDataSourceProviderImpl( - std::unique_ptr<DataSourceFactory> factory) + std::unique_ptr<DataSource::Factory> factory) : data_source_factory_(std::move(factory)) {} HlsDataSourceProviderImpl::~HlsDataSourceProviderImpl() { @@ -107,7 +107,7 @@ auto [new_uri, bypass_cache] = stream->GetNextSegmentURIAndCacheStatus(); TRACE_EVENT_BEGIN("media", "HLS::CreateDataSource", perfetto::Track::FromPointer(this), "uri", new_uri); - data_source_factory_->CreateDataSource( + data_source_factory_->Create( std::move(new_uri), bypass_cache, base::BindOnce(&HlsDataSourceProviderImpl::OnDataSourceCreated, weak_factory_.GetWeakPtr(), std::move(stream), @@ -161,6 +161,7 @@ ReadCb callback, std::unique_ptr<DataSource> data_source) { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + CHECK(data_source); auto stream_id = stream->stream_id(); auto old_data_source = data_source_map_.find(stream_id); if (old_data_source != data_source_map_.end()) { diff --git a/media/filters/hls_data_source_provider_impl.h b/media/filters/hls_data_source_provider_impl.h index 6a3e8fe5..b110c2fe 100644 --- a/media/filters/hls_data_source_provider_impl.h +++ b/media/filters/hls_data_source_provider_impl.h @@ -22,20 +22,9 @@ class MEDIA_EXPORT HlsDataSourceProviderImpl : public HlsDataSourceProvider { public: - // An instance of DataSourceFactory allows separation of DataSource creation - // and DataSourceStream buffer management for easier testing. - class DataSourceFactory { - public: - using DataSourceCb = base::OnceCallback<void(std::unique_ptr<DataSource>)>; - virtual ~DataSourceFactory() = default; - virtual void CreateDataSource(GURL uri, - bool ignore_cache, - DataSourceCb cb) = 0; - }; - ~HlsDataSourceProviderImpl() override; explicit HlsDataSourceProviderImpl( - std::unique_ptr<DataSourceFactory> factory); + std::unique_ptr<DataSource::Factory> factory); // HlsDataSourceProvider implementation void ReadFromCombinedUrlQueue(SegmentQueue segments, @@ -56,7 +45,7 @@ ReadCb callback, bool success); - std::unique_ptr<DataSourceFactory> data_source_factory_; + std::unique_ptr<DataSource::Factory> data_source_factory_; HlsDataSourceStream::StreamId::Generator stream_id_generator_; diff --git a/media/filters/hls_test_helpers.cc b/media/filters/hls_test_helpers.cc index 78ebdae..a9c5323 100644 --- a/media/filters/hls_test_helpers.cc +++ b/media/filters/hls_test_helpers.cc @@ -86,7 +86,9 @@ MockDataSourceFactory::~MockDataSourceFactory() = default; MockDataSourceFactory::MockDataSourceFactory() = default; -void MockDataSourceFactory::CreateDataSource(GURL uri, bool, DataSourceCb cb) { +void MockDataSourceFactory::Create(const GURL&, + bool, + DataSource::DataSourceCb cb) { if (!next_mock_) { PregenerateNextMock(); EXPECT_CALL(*next_mock_, Initialize) diff --git a/media/filters/hls_test_helpers.h b/media/filters/hls_test_helpers.h index 3f2c611..d987645 100644 --- a/media/filters/hls_test_helpers.h +++ b/media/filters/hls_test_helpers.h @@ -209,12 +209,13 @@ bool taint_origin = false); }; -class MockDataSourceFactory - : public HlsDataSourceProviderImpl::DataSourceFactory { +class MockDataSourceFactory : public DataSource::Factory { public: ~MockDataSourceFactory() override; MockDataSourceFactory(); - void CreateDataSource(GURL uri, bool ignore_cache, DataSourceCb cb) override; + void Create(const GURL& uri, + bool ignore_cache, + DataSource::DataSourceCb cb) override; void AddReadExpectation(size_t from, size_t to, int response); testing::NiceMock<MockDataSource>* PregenerateNextMock(); diff --git a/media/test/pipeline_integration_test_base.cc b/media/test/pipeline_integration_test_base.cc index 1d4193e..acf6cac 100644 --- a/media/test/pipeline_integration_test_base.cc +++ b/media/test/pipeline_integration_test_base.cc @@ -78,11 +78,12 @@ #if BUILDFLAG(ENABLE_HLS_DEMUXER) namespace { -class TestDataSourceFactory - : public HlsDataSourceProviderImpl::DataSourceFactory { +class TestDataSourceFactory : public DataSource::Factory { public: ~TestDataSourceFactory() override = default; - void CreateDataSource(GURL uri, bool, DataSourceCb callback) override { + void Create(const GURL& uri, + bool, + DataSource::DataSourceCb callback) override { auto file_data_source = std::make_unique<FileDataSource>(); base::FilePath file_path( #if BUILDFLAG(IS_WIN) diff --git a/third_party/blink/renderer/platform/media/BUILD.gn b/third_party/blink/renderer/platform/media/BUILD.gn index 5b4acbb..df1ccbe 100644 --- a/third_party/blink/renderer/platform/media/BUILD.gn +++ b/third_party/blink/renderer/platform/media/BUILD.gn @@ -112,13 +112,6 @@ "//third_party/blink/renderer/platform", "//third_party/blink/renderer/platform:allow_discouraged_type", ] - - if (enable_hls_demuxer) { - sources += [ - "multi_buffer_data_source_factory.cc", - "multi_buffer_data_source_factory.h", - ] - } } source_set("unit_tests") { diff --git a/third_party/blink/renderer/platform/media/multi_buffer_data_source.cc b/third_party/blink/renderer/platform/media/multi_buffer_data_source.cc index d96ffd2..0af2ad1 100644
Regression Test / PoC
diff --git a/media/test/pipeline_integration_test_base.cc b/media/test/pipeline_integration_test_base.cc
index 1d4193e..acf6cac 100644
--- a/media/test/pipeline_integration_test_base.cc
+++ b/media/test/pipeline_integration_test_base.cc
@@ -78,11 +78,12 @@
#if BUILDFLAG(ENABLE_HLS_DEMUXER)
namespace {
-class TestDataSourceFactory
- : public HlsDataSourceProviderImpl::DataSourceFactory {
+class TestDataSourceFactory : public DataSource::Factory {
public:
~TestDataSourceFactory() override = default;
- void CreateDataSource(GURL uri, bool, DataSourceCb callback) override {
+ void Create(const GURL& uri,
+ bool,
+ DataSource::DataSourceCb callback) override {
auto file_data_source = std::make_unique<FileDataSource>();
base::FilePath file_path(
#if BUILDFLAG(IS_WIN)
Original Bug Report
Cross-origin audio leak in HLS via HTMLMediaElement.captureStream()
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A potential vulnerability exists where an attacker can exfiltrate cross-origin audio from an HLS stream using HTMLMediaElement.captureStream(). Because HLS initialization bypasses standard data source setup, the callback responsible for tainting the audio source provider is never registered. As a result, cross-origin audio segments are not muted in the capture pipeline, leading to a potential Same-Origin Policy bypass.
Affected files:
third_party/blink/renderer/platform/media/web_media_player_impl.ccthird_party/blink/renderer/platform/media/web_audio_source_provider_impl.ccthird_party/blink/renderer/modules/mediacapturefromelement/html_audio_element_capturer_source.ccthird_party/blink/renderer/platform/media/multi_buffer_data_source_factory.ccmedia/filters/demuxer_manager.ccmedia/filters/hls_manifest_demuxer_engine.ccmedia/filters/hls_data_source_provider_impl.cc
Estimated timestamp from git blame: 2026-02-03
Root Cause Analysis
When loading media, WebMediaPlayerImpl::Load() generally creates a primary MultiBufferDataSource and registers a redirect callback via mb_data_source->OnRedirect(blink::BindRepeating(&WebMediaPlayerImpl::OnDataSourceRedirected...)). This callback is critical because OnDataSourceRedirected() is responsible for calling audio_source_provider_->TaintOrigin() if the new origin taints the media.
However, for HLS streams (demuxer_manager_->IsManifestDemuxerURL()), the code takes an early-return path and calls StartPipeline() directly, skipping the primary MultiBufferDataSource creation. Consequently, the OnDataSourceRedirected callback is never registered.
Subsequent HLS segment fetches are handled via MultiBufferDataSourceFactory, but these individual segment data sources are never wired to trigger the taint notification. While the internal HLS engine (HlsManifestDemuxerEngine) correctly aggregates the WouldTaintOrigin() state when cross-origin segments are loaded, WebAudioSourceProviderImpl is never notified. Its internal origin_tainted_ flag remains false.
When an attacker captures the stream using HTMLMediaElement.captureStream(), WebAudioSourceProviderImpl::Render() checks origin_tainted_.IsSet() to determine if it should zero out the audio buffer before handing it to HtmlAudioElementCapturerSource. Because the flag is incorrectly false, the raw, authenticated cross-origin audio is passed directly to the attacker’s script.
Note: Video capture is immune to this specific bypass because HtmlVideoElementCapturerSource::sendNewFrame() actively polls web_media_player_->WouldTaintOrigin() on every frame. HtmlAudioElementCapturerSource lacks this polling and relies entirely on the broken push notification.
Potential Attacker Steps
Note: Our tooling agent cannot execute code, so these are suggested steps based on static analysis of the codebase.
- An attacker hosts a malicious webpage with an
<audio>or<video>element pointing to a same-origin HLS manifest (stream.m3u8). Thecrossoriginattribute is omitted. - The manifest initially serves a same-origin media segment (e.g.,
seg0.ts). - The browser starts playback. Because it is same-origin, the initial origin checks in
MediaElementEventListener::UpdateSources()pass. - The attacker’s JavaScript listens for the
loadedmetadataevent and executesvideo.captureStream(), hooking up aMediaRecorderto the resultingMediaStream. - The HLS manifest transitions to a cross-origin segment containing authenticated audio (e.g.,
https://victim.com/auth_audio.ts). - Because
crossoriginwas not set, the browser fetches the segment inno-corsmode, appending ambient credentials (cookies) forvictim.com. - The cross-origin audio is demuxed. Because
audio_source_provider_->TaintOrigin()was never called, the audio is not muted. - The attacker’s
MediaRecordercaptures the raw audio samples fromvictim.com, achieving a cross-origin data leak.
Suggested Fix
There are two primary ways to resolve this:
- Implement active polling for audio: Update
HtmlAudioElementCapturerSource::OnAudioBus()(or the related callback logic) to pollweb_media_player_->WouldTaintOrigin()on each chunk of audio, similar to howHtmlVideoElementCapturerSource::sendNewFrame()checks it on every video frame. If true, explicitly output silence. - Fix the push notification: Ensure that
HlsManifestDemuxerEngineorHlsDataSourceProviderImplcan trigger a callback toWebMediaPlayerImpl::OnDataSourceRedirected()(or a similar method) whenever the stream’s aggregatedWouldTaintOrigin()status flips fromfalsetotrue.
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.