CVE-2026-17696
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifmedia/filters/hls_manifest_demuxer_engine.cc |
modified |
Files Changed
media/filters/demuxer_manager.ccmedia/filters/demuxer_manager.hmedia/filters/hls_manifest_demuxer_engine.cc
Patch
From eeef37ad17e50c9234cd6a9e1af35f25161dccdc Mon Sep 17 00:00:00 2001
From: Ted Meyer <tmathmeyer@chromium.org>
Date: Fri, 12 Jun 2026 17:22:14 -0700
Subject: [PATCH] Track actual response origin for HLS manifest
HLS manifests track the URI to which the request was made from which the
data for the manifest was gotten, which was always necessary for things
like resolving relative sub-resource URIs. However, it's also important
to track the _actual_ security origin from which the data came, because
the original URI might have issued a redirect to an unsuspecting target.
Bug: 517550034
Bug: 517623783
Change-Id: Icc501ee7965489c532028fc99bf018fff4573ffa
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7904181
Commit-Queue: Ted (Chromium) Meyer <tmathmeyer@chromium.org>
Reviewed-by: Syed AbuTalib <lowkey@google.com>
Cr-Commit-Position: refs/heads/main@{#1646338}
---
diff --git a/media/filters/demuxer_manager.cc b/media/filters/demuxer_manager.cc
index 03dd5e1..dfdb382 100644
--- a/media/filters/demuxer_manager.cc
+++ b/media/filters/demuxer_manager.cc
@@ -67,12 +67,14 @@
DemuxerManager::DemuxerManager(
Client* client,
+ url::Origin security_origin,
scoped_refptr<base::SequencedTaskRunner> media_task_runner,
MediaLog* log,
std::unique_ptr<Demuxer> demuxer_override)
: client_(client),
media_task_runner_(std::move(media_task_runner)),
media_log_(log->Clone()),
+ security_origin_(std::move(security_origin)),
demuxer_override_(std::move(demuxer_override)) {
DCHECK(client_);
}
@@ -449,7 +451,7 @@
&DemuxerManager::SetTrackState, weak_factory_.GetWeakPtr())));
auto engine = std::make_unique<HlsManifestDemuxerEngine>(
client_->GetHlsDataSourceProvider(), media_task_runner_, std::move(m),
- would_taint_origin, loaded_url_, media_log_.get());
+ would_taint_origin, security_origin_, loaded_url_, media_log_.get());
raw_ptr<DataSourceInfo> datasource_info = engine.get();
return std::make_tuple(
diff --git a/media/filters/demuxer_manager.h b/media/filters/demuxer_manager.h
index afcdb31..3455c5c 100644
--- a/media/filters/demuxer_manager.h
+++ b/media/filters/demuxer_manager.h
@@ -91,6 +91,7 @@
bool /*is_static*/)>;
DemuxerManager(Client* client,
+ url::Origin security_origin,
scoped_refptr<base::SequencedTaskRunner> media_task_runner,
MediaLog* log,
std::unique_ptr<Demuxer> demuxer_override);
@@ -183,6 +184,11 @@
// Note: this may be very large, take care when making copies.
GURL loaded_url_;
+ // The security origin of the frame. HLS tracks the security origin for
+ // manifests, and since manifests can exist as data: urls with no security
+ // origin, we need to use the frame origin instead.
+ url::Origin security_origin_;
+
// The data source for creating a demuxer. This should be null when using
// ChunkDemuxer.
std::unique_ptr<DataSource> data_source_;
diff --git a/media/filters/hls_manifest_demuxer_engine.cc b/media/filters/hls_manifest_demuxer_engine.cc
index 7cfa1a0..7af158c2 100644
--- a/media/filters/hls_manifest_demuxer_engine.cc
+++ b/media/filters/hls_manifest_demuxer_engine.cc
@@ -214,10 +214,12 @@
scoped_refptr<base::SequencedTaskRunner> media_task_runner,
std::unique_ptr<TrackManager> track_manager,
bool was_already_tainted,
+ url::Origin security_origin,
GURL root_playlist_uri,
MediaLog* media_log)
: media_task_runner_(std::move(media_task_runner)),
track_manager_(std::move(track_manager)),
+ security_origin_(std::move(security_origin)),
root_playlist_uri_(std::move(root_playlist_uri)),
media_log_(media_log->Clone()),
network_access_(std::make_unique<HlsNetworkAccessImpl>(std::move(dsp))),
@@ -564,6 +566,27 @@
return;
}
auto stream = std::move(maybe_stream).value();
+ std::optional<url::Origin> manifest_origin = std::nullopt;
+
+ switch (stream->SecurityInfo().response_origins.size()) {
+ // A single security origin is the norm, and acceptable.
+ case 1: {
+ manifest_origin = *stream->SecurityInfo().response_origins.begin();
+ break;
+ }
+ case 0: {
+ if (uri.SchemeIs("data")) {
+ // Data URIs have no security origin. Any other url should have one.
+ break;
+ }
+ [[fallthrough]];
+ }
+ default: {
+ std::move(cb).Run({HlsDemuxerStatus::Codes::kInvalidManifest,
+ "Manifest origin was insecurely indeterminate"});
+ return;
+ }
+ }
auto maybe_info = hls::Playlist::IdentifyPlaylist(stream->AsString());
if (!maybe_info.has_value()) {
@@ -579,8 +602,15 @@
return;
}
+ if (!manifest_origin && multivariant_root_) {
+ // Media playlists can be loaded from data urls - in which case we just
+ // use the multivariant origin.
+ manifest_origin = multivariant_root_->SecurityOrigin();
+ }
+
auto maybe_playlist = ParseMediaPlaylistFromStringSource(
- stream->AsString(), std::move(uri), (*maybe_info).version);
+ stream->AsString(), std::move(uri),
+ manifest_origin.value_or(security_origin_), (*maybe_info).version);
if (!maybe_playlist.has_value()) {
auto error = std::move(maybe_playlist).error();
RecordParserFailure(error.code());
@@ -675,9 +705,12 @@
return;
}
auto stream = std::move(m_stream).value();
+ std::optional<url::Origin> manifest_origin = std::nullopt;
+
switch (stream->SecurityInfo().response_origins.size()) {
// A single security origin is the norm, and acceptable.
case 1: {
+ manifest_origin = *stream->SecurityInfo().response_origins.begin();
break;
}
case 0: {
@@ -685,12 +718,12 @@
// Data URIs have no security origin. Any other url should have one.
break;
}
- PERFETTO_FALLTHROUGH;
+ [[fallthrough]];
}
default: {
std::move(parse_complete_cb)
.Run({HlsDemuxerStatus::Codes::kInvalidManifest,
- "Manifest was served over an insecure connection"});
+ "Manifest origin was insecurely indeterminate"});
return;
}
}
@@ -719,7 +752,8 @@
return;
}
auto playlist = hls::MultivariantPlaylist::Parse(
- stream->AsString(), parse_info.uri, (*m_info).version);
+ stream->AsString(), parse_info.uri,
+ manifest_origin.value_or(security_origin_), (*m_info).version);
if (!playlist.has_value()) {
auto error = std::move(playlist).error();
RecordParserFailure(error.code());
@@ -732,8 +766,15 @@
std::move(playlist).value());
}
case hls::Playlist::Kind::kMediaPlaylist: {
+ if (!manifest_origin && multivariant_root_) {
+ // Media playlists can be loaded from data urls - in which case we just
+ // use the multivariant origin.
+ manifest_origin = multivariant_root_->SecurityOrigin();
+ }
+
auto playlist = ParseMediaPlaylistFromStringSource(
- stream->AsString(), parse_info.uri, (*m_info).version);
+ stream->AsString(), parse_info.uri,
+ manifest_origin.value_or(security_origin_), (*m_info).version);
if (!playlist.has_value()) {
auto error = std::move(playlist).error();
RecordParserFailure(error.code());
@@ -753,9 +794,10 @@
HlsManifestDemuxerEngine::ParseMediaPlaylistFromStringSource(
std::string_view source,
GURL uri,
+ const url::Origin& manifest_origin,
hls::types::DecimalInteger version) {
DCHECK_CALLED_ON_VALID_SEQUENCE(media_sequence_checker_);
- return hls::MediaPlaylist::Parse(source, uri, version,
+ return hls::MediaPlaylist::Parse(source, uri, manifest_origin, version,
multivariant_root_.get());
}
Regression Test / PoC
diff --git a/media/filters/hls_manifest_demuxer_engine_unittest.cc b/media/filters/hls_manifest_demuxer_engine_unittest.cc
index 8ca4ef8d..acf4e58 100644
--- a/media/filters/hls_manifest_demuxer_engine_unittest.cc
+++ b/media/filters/hls_manifest_demuxer_engine_unittest.cc
@@ -464,6 +464,7 @@
base::SequenceBound<FakeHlsDataSourceProvider> dsp(
task_environment_.GetMainThreadTaskRunner(), mock_dsp_.get());
+ GURL url = GURL("http://media.example.com/manifest.m3u8");
engine_ = std::make_unique<HlsManifestDemuxerEngine>(
std::move(dsp), base::SingleThreadTaskRunner::GetCurrentDefault(),
std::make_unique<ForwardingTrackManager>(
@@ -473,8 +474,7 @@
base::Unretained(this)),
base::BindRepeating(&HlsManifestDemuxerEngineTest::SetTrackState,
base::Unretained(this))),
- false, GURL("http://media.example.com/manifest.m3u8"),
- media_log_.get());
+ false, url::Origin::Create(url), url, media_log_.get());
}
void InitializeEngine() {
diff --git a/media/filters/hls_network_access_impl_unittest.cc b/media/filters/hls_network_access_impl_unittest.cc
index 0a8b7b6..86114dd 100644
--- a/media/filters/hls_network_access_impl_unittest.cc
+++ b/media/filters/hls_network_access_impl_unittest.cc
@@ -63,7 +63,8 @@
init = base::MakeRefCounted<hls::MediaSegment::InitializationSegment>(
GURL("https://foo.com"), ByteRangeFromTuple(init_br));
}
- auto manifest_uri = GURL("https://example.com");
+ auto manifest_uri = GURL("https://example.com/manifest.m3u8");
+ auto resource_uri = GURL("https://example.com/content.mp4");
if (key_location.has_value()) {
auto key_uri = GURL(*key_location);
enc_data = base::MakeRefCounted<hls::MediaSegment::EncryptionData>(
@@ -74,9 +75,9 @@
: hls::MediaSegment::EncryptionData::KeyLocation::kUnsafeOrigin);
}
return base::MakeRefCounted<hls::MediaSegment>(
- base::Seconds(1), 0, 0, manifest_uri, std::move(init),
- std::move(enc_data), ByteRangeFromTuple(byte_range), std::nullopt,
- false, false, init_mode == InitMode::kPresent, false);
+ base::Seconds(1), 0, 0, resource_uri, url::Origin::Create(manifest_uri),
+ std::move(init), std::move(enc_data), ByteRangeFromTuple(byte_range),
+ std::nullopt, false, false, init_mode == InitMode::kPresent, false);
}
protected:
@@ -435,7 +436,7 @@
DataSource::CacheMode::kHitCache,
DataSource::EncodingMode::kIdentity))
.Times(1);
- EXPECT_CALL(*factory_, MockCreate(GURL("https://example.com/"),
+ EXPECT_CALL(*factory_, MockCreate(GURL("https://example.com/content.mp4"),
DataSource::CacheMode::kHitCache,
DataSource::EncodingMode::kIdentity))
.Times(1);
diff --git a/media/filters/hls_rendition_impl_unittest.cc b/media/filters/hls_rendition_impl_unittest.cc
index c2c865e..0a856338 100644
--- a/media/filters/hls_rendition_impl_unittest.cc
+++ b/media/filters/hls_rendition_impl_unittest.cc
@@ -320,7 +320,8 @@
std::unique_ptr<HlsRenditionImpl> MakeVodRendition(std::string_view content) {
constexpr hls::types::DecimalInteger version = 3;
auto uri = GURL("https://example.com/manifest.m3u8");
- auto parsed = hls::MediaPlaylist::Parse(content, uri, version, nullptr);
+ auto parsed = hls::MediaPlaylist::Parse(
+ content, uri, url::Origin::Create(uri), version, nullptr);
if (!parsed.has_value()) {
LOG(ERROR) << MediaSerializeForTesting(std::move(parsed).error());
return nullptr;
@@ -338,7 +339,8 @@
GURL uri,
std::string_view content) {
constexpr hls::types::DecimalInteger version = 3;
- auto parsed = hls::MediaPlaylist::Parse(content, uri, version, nullptr);
+ auto parsed = hls::MediaPlaylist::Parse(
+ content, uri, url::Origin::Create(uri), version, nullptr);
if (!parsed.has_value()) {
LOG(ERROR) << MediaSerializeForTesting(std::move(parsed).error());
return nullptr;
@@ -666,8 +668,8 @@
}
TEST_F(HlsRenditionImplUnittest, TestRenditionHasEnoughDataDeleteOldContent) {
- auto rendition =
- MakeLiveRendition(GURL("http://example.com"), kInitialFetchPlaylist);
+ auto manifest_uri = GURL("http://example.com");
+ auto rendition = MakeLiveRendition(manifest_uri, kInitialFetchPlaylist);
ASSERT_NE(rendition, nullptr);
ASSERT_EQ(rendition->GetDuration(), std::nullopt);
@@ -687,10 +689,11 @@
// There are only three segments (6 seconds) left in the buffer, so we'll
// pull for manifest updates.
EXPECT_CALL(*mock_hrh_, UpdateRenditionManifestUri("test", _, _))
- .WillOnce([&rendition](std::string role, GURL uri,
- HlsDemuxerStatusCallback cb) {
+ .WillOnce([&rendition, &manifest_uri](std::string role, GURL uri,
+ HlsDemuxerStatusCallback cb) {
auto parsed = hls::MediaPlaylist::Parse(
- kSecondFetchLivePlaylist, GURL("http://example.com"), 3, nullptr);
+ kSecondFetchLivePlaylist, manifest_uri,
+ url::Origin::Create(manifest_uri), 3, nullptr);
CHECK(parsed.has_value());
rendition->UpdatePlaylist(std::move(parsed).value());
std::move(cb).Run(OkStatus());
@@ -715,8 +718,8 @@
}
TEST_F(HlsRenditionImplUnittest, TestPauseAndUnpause) {
- auto rendition =
- MakeLiveRendition(GURL("http://example.com"), kInitialFetchLongPlaylist);
+ auto manifest_uri = GURL("http://example.com");
+ auto rendition = MakeLiveRendition(manifest_uri, kInitialFetchLongPlaylist);
ASSERT_NE(rendition, nullptr);
ASSERT_EQ(rendition->GetDuration(), std::nullopt);
@@ -780,11 +783,11 @@
std::string newcontent = "newcontent";
EXPECT_CALL(*mock_mdeh_, Remove(_, base::Seconds(0), base::Seconds(210)));
EXPECT_CALL(*mock_hrh_, UpdateRenditionManifestUri("test", _, _))
- .WillOnce([&rendition](std::string role, GURL uri,
- HlsDemuxerStatusCallback cb) {
- auto parsed =
- hls::MediaPlaylist::Parse(kSecondFetchLiveLongPlaylist,
- GURL("http://example.com"), 3, nullptr);
+ .WillOnce([&rendition, &manifest_uri](std::string role, GURL uri,
+ HlsDemuxerStatusCallback cb) {
+ auto parsed = hls::MediaPlaylist::Parse(
+ kSecondFetchLiveLongPlaylist, manifest_uri,
+ url::Origin::Create(manifest_uri), 3, nullptr);
CHECK(parsed.has_value());
rendition->UpdatePlaylist(std::move(parsed).value());
std::move(cb).Run(OkStatus());
@@ -970,8 +973,9 @@
// Update the playlist. The segment stream should keep around media_3.ts,
// but follow it up with mediax_4.ts
GURL manifest_uri = GURL("https://example.com/manifest.m3u8");
- auto parsed = hls::MediaPlaylist::Parse(kAESContentReplacement, manifest_uri,
- 3, nullptr);
+ auto parsed =
+ hls::MediaPlaylist::Parse(kAESContentReplacement, manifest_uri,
+ url::Origin::Create(manifest_uri), 3, nullptr);
CHECK(parsed.has_value());
rendition->UpdatePlaylist(std::move(parsed).value());
@@ -1148,8 +1152,8 @@
}
TEST_F(HlsRenditionImplUnittest, SeekWithBadContentCausesError) {
- auto rendition =
- MakeLiveRendition(GURL("http://example.com"), kInitialFetchLongPlaylist);
+ auto manifest_uri = GURL("http://example.com");
+ auto rendition = MakeLiveRendition(manifest_uri, kInitialFetchLongPlaylist);
ASSERT_NE(rendition, nullptr);
ASSERT_EQ(rendition->GetDuration(), std::nullopt);
@@ -1205,10 +1209,11 @@
EXPECT_CALL(*mock_mdeh_, Remove(_, base::Seconds(0), base::Seconds(210)));
EXPECT_CALL(*mock_hrh_, UpdateRenditionManifestUri("test", _, _))
- .WillOnce([&rendition](std::string role, GURL uri,
- HlsDemuxerStatusCallback cb) {
+ .WillOnce([&rendition, &manifest_uri](std::string role, GURL uri,
+ HlsDemuxerStatusCallback cb) {
auto parsed = hls::MediaPlaylist::Parse(
- kSingleSegmentPlaylist, GURL("http://example.com"), 3, nullptr);
+ kSingleSegmentPlaylist, manifest_uri,
+ url::Origin::Create(manifest_uri), 3, nullptr);
CHECK(parsed.has_value());
rendition->UpdatePlaylist(std::move(parsed).value());
std::move(cb).Run(OkStatus());
@@ -1307,8 +1312,8 @@
}
TEST_F(HlsRenditionImplUnittest, TestLiveToVodAdaptation) {
- auto rendition =
- MakeLiveRendition(GURL("http://example.com"), kInitialFetchLongPlaylist);
+ auto manifest_uri = GURL("http://example.com");
+ auto rendition = MakeLiveRendition(manifest_uri, kInitialFetchLongPlaylist);
ASSERT_NE(rendition, nullptr);
ASSERT_EQ(rendition->GetDuration(), std::nullopt);
base::TimeDelta clock = base::Seconds(0);
@@ -1349,10 +1354,11 @@
{
RespondWithRange(base::Seconds(0), base::Seconds(30));
EXPECT_CALL(*mock_hrh_, UpdateRenditionManifestUri("test", _, _))
- .WillOnce([&rendition](std::string role, GURL uri,
- HlsDemuxerStatusCallback cb) {
+ .WillOnce([&rendition, manifest_uri](std::string role, GURL uri,
+ HlsDemuxerStatusCallback cb) {
auto parsed = hls::MediaPlaylist::Parse(
- kNowAsVodPlaylist, GURL("http://example.com"), 3, nullptr);
+ kNowAsVodPlaylist, manifest_uri,
+ url::Origin::Create(manifest_uri), 3, nullptr);
CHECK(parsed.has_value());
rendition->UpdatePlaylist(std::move(parsed).value());
std::move(cb).Run(OkStatus());
@@ -1372,8 +1378,8 @@
}
TEST_F(HlsRenditionImplUnittest, TestLiveToVodAdaptationWithExhaustedQueue) {
- auto rendition =
- MakeLiveRendition(GURL("http://example.com"), kInitialFetchLongPlaylist);
+ auto manifest_uri = GURL("http://example.com");
+ auto rendition = MakeLiveRendition(manifest_uri, kInitialFetchLongPlaylist);
ASSERT_NE(rendition, nullptr);
ASSERT_EQ(rendition->GetDuration(), std::nullopt);
base::TimeDelta clock = base::Seconds(0);
@@ -1414,11 +1420,11 @@
{
RespondWithRange(base::Seconds(0), base::Seconds(30));
EXPECT_CALL(*mock_hrh_, UpdateRenditionManifestUri("test", _, _))
- .WillOnce([&rendition](std::string role, GURL uri,
- HlsDemuxerStatusCallback cb) {
- auto parsed =
- hls::MediaPlaylist::Parse(kNowAsVodPlaylistWithOneMore,
- GURL("http://example.com"), 3, nullptr);
+ .WillOnce([&rendition, &manifest_uri](std::string role, GURL uri,
+ HlsDemuxerStatusCallback cb) {
+ auto parsed = hls::MediaPlaylist::Parse(
+ kNowAsVodPlaylistWithOneMore, manifest_uri,
+ url::Origin::Create(manifest_uri), 3, nullptr);
CHECK(parsed.has_value());
rendition->UpdatePlaylist(std::move(parsed).value());
std::move(cb).Run(OkStatus());
diff --git a/media/formats/hls/media_playlist_unittest.cc b/media/formats/hls/media_playlist_unittest.cc
index ac9e40b..c2deac64 100644
--- a/media/formats/hls/media_playlist_unittest.cc
+++ b/media/formats/hls/media_playlist_unittest.cc
@@ -36,7 +36,9 @@
// Parse the given source. Failure here isn't supposed to be part of the test,
// so use a CHECK.
- auto result = MultivariantPlaylist::Parse(source, std::move(uri), version);
+ auto origin = url::Origin::Create(uri);
+ auto result =
+ MultivariantPlaylist::Parse(source, std::move(uri), origin, version);
CHECK(result.has_value());
return std::move(result).value();
}
diff --git a/media/test/pipeline_integration_test_base.cc b/media/test/pipeline_integration_test_base.cc
index a99fcb0..58b7154 100644
--- a/media/test/pipeline_integration_test_base.cc
+++ b/media/test/pipeline_integration_test_base.cc
@@ -397,7 +397,8 @@
std::move(hls_dsp), task_environment_.GetMainThreadTaskRunner(),
std::make_unique<ForwardingTrackManager>(
base::DoNothing(), base::DoNothing(), base::DoNothing()),
- /*name=*/false, manifest_root, &media_log_);
+ /*name=*/false, url::Origin::Create(manifest_root), manifest_root,
+ &media_log_);
demuxer_ = std::make_unique<ManifestDemuxer>(
task_environment_.GetMainThreadTaskRunner(), base::DoNothing(),
std::move(engine), &media_log_);
Original Bug Report
Cross-origin padding oracle in HLS demuxer via EXT-X-MAP tag
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A potential cross-origin padding oracle in Chromium’s HLS demuxer may allow a web attacker to leak the length and contents of arbitrary cross-origin resources. By omitting the BYTERANGE attribute on an EXT-X-MAP tag, the security check designed to block cross-origin range requests is bypassed. An attacker could observe decryption success or failure via JavaScript-visible media elements to infer the block-alignment of cross-origin data.
Affected files:
media/formats/hls/media_segment.ccmedia/filters/hls_data_source_provider_impl.ccmedia/filters/hls_manifest_demuxer_engine.cc
Estimated timestamp from git blame: 2026-04-14
Detailed Description
A potential vulnerability in Chromium’s HLS (HTTP Live Streaming) implementation could allow a malicious webpage to leak information about cross-origin resources.
In media/formats/hls/media_segment.cc, MediaSegment::GetPlaintextStreamSource decrypts a combined stream buffer containing the concatenation of the EXT-X-MAP initialization segment and the media segment as a single AES-128-CBC block:
auto maybe_plaintext = crypto::aes_cbc::Decrypt(key, iv, src);
if (!maybe_plaintext) { return false; }
If the total buffer is not a multiple of 16 bytes or does not contain valid PKCS#7 padding, the decryption fails, returning std::nullopt and causing a demuxer error. This behavior behaves as a padding oracle.
Security Check Bypass
The HLS demuxer uses the following check to block range requests on cross-origin content:
bool HasIncompatibleRangeAndOrigin() const {
return would_taint_origin_ && requires_range_request_;
}
However, this security gate only triggers when requires_range_request_ is set to true. When a playlist uses #EXT-X-MAP:URI="https://victim.example/secret" with no BYTERANGE attribute, the data source provider sets the range mode to DataSource::RangeMode::kFullRequest. As a result, requires_range_request_ remains false, bypassing the origin check entirely and allowing cross-origin resources to be loaded.
Potential Attack Scenario
(Note: These are potential steps; no functional exploit has been run or verified by our tooling)
- An attacker hosts a webpage that embeds a
<video>element pointing to a malicious playlist. - The playlist references a cross-origin initialization segment (
EXT-X-MAP) with noBYTERANGEattribute and an attacker-controlled same-origin media segment. - The HLS demuxer downloads both resources into a single buffer of size $N$ (victim segment size) $+$ $M$ (attacker segment size).
- The attacker systematically varies the media segment size $M$ from 32 to 47 bytes.
- If the combined size $N + M$ is not a multiple of 16, or does not have valid padding, decryption fails and triggers an
errorevent on the media element. - If decryption succeeds and the container magic matches (which can be brute-forced by sweeping the first byte of the Initialization Vector), JavaScript-visible events such as
loadedmetadataare fired. - By monitoring these events, the attacker can infer the length modulo 16 of the cross-origin target and potentially decrypt trailing bytes using standard padding oracle techniques.
Suggested Remediation
To remediate this issue, Chromium should:
- Restrict the
EXT-X-MAPtag to the same origin as the playlist unless CORS is explicitly allowed by the target server. - Ensure that cross-origin segments are not concatenated or decrypted in the same buffer as same-origin segments.
- Improve
HasIncompatibleRangeAndOrigin()to robustly prevent tainted origins from loading arbitrary data chunks without proper CORS validation, regardless of whether a byte range is requested.
Evaluated with Chrome root at commit: 5133b93d189b383c37805b1cf3a9d2dbfe8d7379
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
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.