CVE-2026-8017
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifmedia/filters/hls_network_access_impl_unittest.cc |
modified | |
BindOncemedia/filters/hls_network_access_impl_unittest.cc |
modified | |
TEST_Fmedia/filters/hls_network_access_impl_unittest.cc |
modified |
Files Changed
media/filters/hls_network_access_impl.ccmedia/filters/hls_network_access_impl_unittest.cc
Patch
From db46e35db798aeee6cea733fcb31f5d8680d6416 Mon Sep 17 00:00:00 2001
From: Ted Meyer <tmathmeyer@chromium.org>
Date: Thu, 02 Apr 2026 11:23:13 -0700
Subject: [PATCH] Restrict origins for HLS keys
HLS encrypted content should not be able to use a key which was loaded
from a source that is "insecure". A key-source is considered secure when
it meets one of these requirements:
- it is not a cross-origin key (hosted on the same domain as the frame
in which it is being accessed)
- it is a data:// url (fully embedded in the manifest)
- it is on the same domain as the manifest from which it is loaded.
- it has the proper Access-Control-Allow-Origin header for the top
frame in which the media is being played.
Bug: 497722578
Change-Id: Id54803fc48b8417ab31e1041f15f0e3acb6225b6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7718533
Commit-Queue: Ted (Chromium) Meyer <tmathmeyer@chromium.org>
Reviewed-by: Dale Curtis <dalecurtis@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1609376}
---
diff --git a/media/filters/hls_network_access_impl.cc b/media/filters/hls_network_access_impl.cc
index 9af1324..3d1bdb5 100644
--- a/media/filters/hls_network_access_impl.cc
+++ b/media/filters/hls_network_access_impl.cc
@@ -53,7 +53,18 @@
std::move(cb).Run(std::move(result).error().AddHere());
return;
}
+
auto stream = std::move(result).value();
+ if (stream->would_taint_origin() &&
+ enc_data->GetKeyLocation() ==
+ hls::MediaSegment::EncryptionData::KeyLocation::kUnsafeOrigin) {
+ // Do not accept keys which would taint the origin, unless it is on the same
+ // origin as the manifest which includes the key.
+ std::move(cb).Run({HlsDataSourceProvider::ReadStatus::Codes::kError,
+ "insecure key request"});
+ return;
+ }
+
enc_data->ImportKey(stream->AsString());
if (enc_data->NeedsKeyFetch()) {
std::move(cb).Run({HlsDataSourceProvider::ReadStatus::Codes::kError,
diff --git a/media/filters/hls_network_access_impl_unittest.cc b/media/filters/hls_network_access_impl_unittest.cc
index 895763e41..8cfd990 100644
--- a/media/filters/hls_network_access_impl_unittest.cc
+++ b/media/filters/hls_network_access_impl_unittest.cc
@@ -15,11 +15,6 @@
namespace {
-enum class KeyMode {
- kPresent,
- kAbsent,
-};
-
enum class InitMode {
kPresent,
kAbsent,
@@ -61,20 +56,25 @@
std::optional<std::tuple<uint64_t, uint64_t>> byte_range,
std::optional<std::tuple<uint64_t, uint64_t>> init_br,
InitMode init_mode = InitMode::kAbsent,
- KeyMode key_mode = KeyMode::kAbsent) {
+ std::optional<std::string> key_location = std::nullopt) {
scoped_refptr<hls::MediaSegment::InitializationSegment> init = nullptr;
scoped_refptr<hls::MediaSegment::EncryptionData> enc_data = nullptr;
if (init_mode == InitMode::kPresent) {
init = base::MakeRefCounted<hls::MediaSegment::InitializationSegment>(
GURL("https://foo.com"), ByteRangeFromTuple(init_br));
}
- if (key_mode == KeyMode::kPresent) {
+ auto manifest_uri = GURL("https://example.com");
+ if (key_location.has_value()) {
+ auto key_uri = GURL(*key_location);
enc_data = base::MakeRefCounted<hls::MediaSegment::EncryptionData>(
- GURL("https://example.com/enc.key"), hls::XKeyTagMethod::kAES128,
- hls::XKeyTagKeyFormat::kIdentity, std::make_tuple(0, 0));
+ GURL(*key_location), hls::XKeyTagMethod::kAES128,
+ hls::XKeyTagKeyFormat::kIdentity, std::make_tuple(0, 0),
+ key_uri.host() == manifest_uri.host()
+ ? hls::MediaSegment::EncryptionData::KeyLocation::kSafeOrigin
+ : hls::MediaSegment::EncryptionData::KeyLocation::kUnsafeOrigin);
}
return base::MakeRefCounted<hls::MediaSegment>(
- base::Seconds(1), 0, 0, GURL("https://example.com"), std::move(init),
+ 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);
}
@@ -131,7 +131,7 @@
factory_->AddReadExpectation(1000, 16384, 0);
network_access_->ReadMediaSegment(
- *segment, /*read_chunked=*/false, /*include_init=*/true,
+ *segment, /*read_chunked=*/false, /*include_init_segment=*/true,
base::BindOnce([&](HlsDataSourceProvider::ReadResult result) {
ASSERT_TRUE(result.has_value());
auto stream = std::move(result).value();
@@ -153,7 +153,7 @@
factory_->AddReadExpectation(49153, 16384, 0);
network_access_->ReadMediaSegment(
- *segment, /*read_chunked=*/false, /*include_init=*/true,
+ *segment, /*read_chunked=*/false, /*include_init_segment=*/true,
base::BindOnce([](HlsDataSourceProvider::ReadResult result) {
ASSERT_TRUE(result.has_value());
auto stream = std::move(result).value();
@@ -177,7 +177,7 @@
factory_->PregenerateNextMock();
network_access_->ReadMediaSegment(
- *segment, /*read_chunked=*/false, /*include_init=*/true,
+ *segment, /*read_chunked=*/false, /*include_init_segment=*/true,
base::BindOnce([](HlsDataSourceProvider::ReadResult result) {
ASSERT_TRUE(result.has_value());
auto stream = std::move(result).value();
@@ -202,7 +202,7 @@
factory_->PregenerateNextMock();
network_access_->ReadMediaSegment(
- *segment, /*read_chunked=*/false, /*include_init=*/true,
+ *segment, /*read_chunked=*/false, /*include_init_segment=*/true,
base::BindOnce([](HlsDataSourceProvider::ReadResult result) {
ASSERT_TRUE(result.has_value());
auto stream = std::move(result).value();
@@ -220,7 +220,7 @@
factory_->AddReadExpectation(100, 100, 100);
network_access_->ReadMediaSegment(
- *segment, /*read_chunked=*/false, /*include_init=*/true,
+ *segment, /*read_chunked=*/false, /*include_init_segment=*/true,
base::BindOnce([&](HlsDataSourceProvider::ReadResult result) {
ASSERT_TRUE(result.has_value());
auto stream = std::move(result).value();
@@ -244,7 +244,7 @@
factory_->AddReadExpectation(98404, 1696, 1696);
network_access_->ReadMediaSegment(
- *segment, /*read_chunked=*/false, /*include_init=*/true,
+ *segment, /*read_chunked=*/false, /*include_init_segment=*/true,
base::BindOnce([&](HlsDataSourceProvider::ReadResult result) {
ASSERT_TRUE(result.has_value());
auto stream = std::move(result).value();
@@ -262,7 +262,7 @@
factory_->AddReadExpectation(100, 16384, 16384);
network_access_->ReadMediaSegment(
- *segment, /*read_chunked=*/true, /*include_init=*/true,
+ *segment, /*read_chunked=*/true, /*include_init_segment=*/true,
base::BindOnce([&](HlsDataSourceProvider::ReadResult result) {
ASSERT_TRUE(result.has_value());
auto stream = std::move(result).value();
@@ -276,7 +276,7 @@
TEST_F(HlsNetworkAccessImplUnittest, TestSegmentWithKey) {
auto segment = MakeSegment(std::nullopt, std::nullopt, InitMode::kAbsent,
- KeyMode::kPresent);
+ "https://example.com/enc.key");
// This actually has to be 16 non-zero bytes.
auto* ds_for_keyfetch = factory_->PregenerateNextMock();
@@ -289,6 +289,8 @@
});
EXPECT_CALL(*ds_for_keyfetch, Read(16, SpanSizeEq(16384), _))
.WillOnce(base::test::RunOnceCallback<2>(0));
+ EXPECT_CALL(*ds_for_keyfetch, WouldTaintOrigin())
+ .WillRepeatedly(testing::Return(true));
// Then expect media content to be read.
factory_->AddReadExpectation(0, 16384, 1000);
@@ -297,7 +299,7 @@
ASSERT_NE(segment->GetEncryptionData(), nullptr);
ASSERT_TRUE(segment->GetEncryptionData()->NeedsKeyFetch());
network_access_->ReadMediaSegment(
- *segment, /*read_chunked=*/false, /*include_init=*/true,
+ *segment, /*read_chunked=*/false, /*include_init_segment=*/true,
base::BindOnce(
[&](scoped_refptr<hls::MediaSegment> segment,
HlsDataSourceProvider::ReadResult result) {
@@ -314,4 +316,43 @@
task_environment_.RunUntilIdle();
}
+TEST_F(HlsNetworkAccessImplUnittest, TestSegmentWithCORSKey) {
+ auto segment = MakeSegment(std::nullopt, std::nullopt, InitMode::kAbsent,
+ "https://example.net/enc.key");
+
+ // This actually has to be 16 non-zero bytes.
+ auto* ds_for_keyfetch = factory_->PregenerateNextMock();
+ EXPECT_CALL(*ds_for_keyfetch, Initialize)
+ .WillOnce(base::test::RunOnceCallback<0>(true));
+ EXPECT_CALL(*ds_for_keyfetch, Read(0, SpanSizeEq(16384), _))
+ .WillOnce([](int64_t, base::span<uint8_t> data, DataSource::ReadCB cb) {
+ std::ranges::fill(data.first<16>(), 'x');
+ std::move(cb).Run(16);
+ });
Regression Test / PoC
diff --git a/media/filters/hls_network_access_impl_unittest.cc b/media/filters/hls_network_access_impl_unittest.cc
index 895763e41..8cfd990 100644
--- a/media/filters/hls_network_access_impl_unittest.cc
+++ b/media/filters/hls_network_access_impl_unittest.cc
@@ -15,11 +15,6 @@
namespace {
-enum class KeyMode {
- kPresent,
- kAbsent,
-};
-
enum class InitMode {
kPresent,
kAbsent,
@@ -61,20 +56,25 @@
std::optional<std::tuple<uint64_t, uint64_t>> byte_range,
std::optional<std::tuple<uint64_t, uint64_t>> init_br,
InitMode init_mode = InitMode::kAbsent,
- KeyMode key_mode = KeyMode::kAbsent) {
+ std::optional<std::string> key_location = std::nullopt) {
scoped_refptr<hls::MediaSegment::InitializationSegment> init = nullptr;
scoped_refptr<hls::MediaSegment::EncryptionData> enc_data = nullptr;
if (init_mode == InitMode::kPresent) {
init = base::MakeRefCounted<hls::MediaSegment::InitializationSegment>(
GURL("https://foo.com"), ByteRangeFromTuple(init_br));
}
- if (key_mode == KeyMode::kPresent) {
+ auto manifest_uri = GURL("https://example.com");
+ if (key_location.has_value()) {
+ auto key_uri = GURL(*key_location);
enc_data = base::MakeRefCounted<hls::MediaSegment::EncryptionData>(
- GURL("https://example.com/enc.key"), hls::XKeyTagMethod::kAES128,
- hls::XKeyTagKeyFormat::kIdentity, std::make_tuple(0, 0));
+ GURL(*key_location), hls::XKeyTagMethod::kAES128,
+ hls::XKeyTagKeyFormat::kIdentity, std::make_tuple(0, 0),
+ key_uri.host() == manifest_uri.host()
+ ? hls::MediaSegment::EncryptionData::KeyLocation::kSafeOrigin
+ : hls::MediaSegment::EncryptionData::KeyLocation::kUnsafeOrigin);
}
return base::MakeRefCounted<hls::MediaSegment>(
- base::Seconds(1), 0, 0, GURL("https://example.com"), std::move(init),
+ 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);
}
@@ -131,7 +131,7 @@
factory_->AddReadExpectation(1000, 16384, 0);
network_access_->ReadMediaSegment(
- *segment, /*read_chunked=*/false, /*include_init=*/true,
+ *segment, /*read_chunked=*/false, /*include_init_segment=*/true,
base::BindOnce([&](HlsDataSourceProvider::ReadResult result) {
ASSERT_TRUE(result.has_value());
auto stream = std::move(result).value();
@@ -153,7 +153,7 @@
factory_->AddReadExpectation(49153, 16384, 0);
network_access_->ReadMediaSegment(
- *segment, /*read_chunked=*/false, /*include_init=*/true,
+ *segment, /*read_chunked=*/false, /*include_init_segment=*/true,
base::BindOnce([](HlsDataSourceProvider::ReadResult result) {
ASSERT_TRUE(result.has_value());
auto stream = std::move(result).value();
@@ -177,7 +177,7 @@
factory_->PregenerateNextMock();
network_access_->ReadMediaSegment(
- *segment, /*read_chunked=*/false, /*include_init=*/true,
+ *segment, /*read_chunked=*/false, /*include_init_segment=*/true,
base::BindOnce([](HlsDataSourceProvider::ReadResult result) {
ASSERT_TRUE(result.has_value());
auto stream = std::move(result).value();
@@ -202,7 +202,7 @@
factory_->PregenerateNextMock();
network_access_->ReadMediaSegment(
- *segment, /*read_chunked=*/false, /*include_init=*/true,
+ *segment, /*read_chunked=*/false, /*include_init_segment=*/true,
base::BindOnce([](HlsDataSourceProvider::ReadResult result) {
ASSERT_TRUE(result.has_value());
auto stream = std::move(result).value();
@@ -220,7 +220,7 @@
factory_->AddReadExpectation(100, 100, 100);
network_access_->ReadMediaSegment(
- *segment, /*read_chunked=*/false, /*include_init=*/true,
+ *segment, /*read_chunked=*/false, /*include_init_segment=*/true,
base::BindOnce([&](HlsDataSourceProvider::ReadResult result) {
ASSERT_TRUE(result.has_value());
auto stream = std::move(result).value();
@@ -244,7 +244,7 @@
factory_->AddReadExpectation(98404, 1696, 1696);
network_access_->ReadMediaSegment(
- *segment, /*read_chunked=*/false, /*include_init=*/true,
+ *segment, /*read_chunked=*/false, /*include_init_segment=*/true,
base::BindOnce([&](HlsDataSourceProvider::ReadResult result) {
ASSERT_TRUE(result.has_value());
auto stream = std::move(result).value();
@@ -262,7 +262,7 @@
factory_->AddReadExpectation(100, 16384, 16384);
network_access_->ReadMediaSegment(
- *segment, /*read_chunked=*/true, /*include_init=*/true,
+ *segment, /*read_chunked=*/true, /*include_init_segment=*/true,
base::BindOnce([&](HlsDataSourceProvider::ReadResult result) {
ASSERT_TRUE(result.has_value());
auto stream = std::move(result).value();
@@ -276,7 +276,7 @@
TEST_F(HlsNetworkAccessImplUnittest, TestSegmentWithKey) {
auto segment = MakeSegment(std::nullopt, std::nullopt, InitMode::kAbsent,
- KeyMode::kPresent);
+ "https://example.com/enc.key");
// This actually has to be 16 non-zero bytes.
auto* ds_for_keyfetch = factory_->PregenerateNextMock();
@@ -289,6 +289,8 @@
});
EXPECT_CALL(*ds_for_keyfetch, Read(16, SpanSizeEq(16384), _))
.WillOnce(base::test::RunOnceCallback<2>(0));
+ EXPECT_CALL(*ds_for_keyfetch, WouldTaintOrigin())
+ .WillRepeatedly(testing::Return(true));
// Then expect media content to be read.
factory_->AddReadExpectation(0, 16384, 1000);
@@ -297,7 +299,7 @@
ASSERT_NE(segment->GetEncryptionData(), nullptr);
ASSERT_TRUE(segment->GetEncryptionData()->NeedsKeyFetch());
network_access_->ReadMediaSegment(
- *segment, /*read_chunked=*/false, /*include_init=*/true,
+ *segment, /*read_chunked=*/false, /*include_init_segment=*/true,
base::BindOnce(
[&](scoped_refptr<hls::MediaSegment> segment,
HlsDataSourceProvider::ReadResult result) {
@@ -314,4 +316,43 @@
task_environment_.RunUntilIdle();
}
+TEST_F(HlsNetworkAccessImplUnittest, TestSegmentWithCORSKey) {
+ auto segment = MakeSegment(std::nullopt, std::nullopt, InitMode::kAbsent,
+ "https://example.net/enc.key");
+
+ // This actually has to be 16 non-zero bytes.
+ auto* ds_for_keyfetch = factory_->PregenerateNextMock();
+ EXPECT_CALL(*ds_for_keyfetch, Initialize)
+ .WillOnce(base::test::RunOnceCallback<0>(true));
+ EXPECT_CALL(*ds_for_keyfetch, Read(0, SpanSizeEq(16384), _))
+ .WillOnce([](int64_t, base::span<uint8_t> data, DataSource::ReadCB cb) {
+ std::ranges::fill(data.first<16>(), 'x');
+ std::move(cb).Run(16);
+ });
+ EXPECT_CALL(*ds_for_keyfetch, Read(16, SpanSizeEq(16384), _))
+ .WillOnce(base::test::RunOnceCallback<2>(0));
+ EXPECT_CALL(*ds_for_keyfetch, WouldTaintOrigin())
+ .WillRepeatedly(testing::Return(true));
+
+ // Then expect media content to be read.
+ factory_->AddReadExpectation(0, 16384, 1000);
+ factory_->AddReadExpectation(1000, 16384, 0);
+
+ ASSERT_NE(segment->GetEncryptionData(), nullptr);
+ ASSERT_TRUE(segment->GetEncryptionData()->NeedsKeyFetch());
+ network_access_->ReadMediaSegment(
+ *segment, /*read_chunked=*/false, /*include_init_segment=*/true,
+ base::BindOnce(
+ [&](scoped_refptr<hls::MediaSegment> segment,
+ HlsDataSourceProvider::ReadResult result) {
+ // The key was hosted on example.net while the manifest is hosted
+ // on example.com. The example.net request did not provide a
+ // Access-Control-Allow-Origin header in it's request, so the use
+ // of the key is blocked, and the segment cannot be decrypted.
+ ASSERT_FALSE(result.has_value());
+ },
+ segment));
+ task_environment_.RunUntilIdle();
+}
+
} // namespace media
diff --git a/media/formats/hls/media_playlist_unittest.cc b/media/formats/hls/media_playlist_unittest.cc
index 505f656e..a8f97424 100644
--- a/media/formats/hls/media_playlist_unittest.cc
+++ b/media/formats/hls/media_playlist_unittest.cc
@@ -1494,7 +1494,8 @@
builder.ExpectSegment(
HasEncryptionData,
std::make_tuple(GURL("http://localhost/enc.key"), XKeyTagMethod::kAES128,
- XKeyTagKeyFormat::kIdentity, std::make_tuple(0, 0x42)));
+ XKeyTagKeyFormat::kIdentity, std::make_tuple(0, 0x42),
+ MediaSegment::EncryptionData::KeyLocation::kSafeOrigin));
builder.ExpectOk();
builder.AppendLine("#EXT-X-KEY:METHOD=NONE");
@@ -1516,7 +1517,8 @@
builder.ExpectSegment(
HasEncryptionData,
std::make_tuple(GURL("http://localhost/enc.key"), XKeyTagMethod::kAES128,
- XKeyTagKeyFormat::kIdentity, std::make_tuple(0, 3)));
+ XKeyTagKeyFormat::kIdentity, std::make_tuple(0, 3),
+ MediaSegment::EncryptionData::KeyLocation::kSafeOrigin));
builder.ExpectOk();
builder.AppendLine("#EXTINF:1.600000,");
@@ -1527,7 +1529,8 @@
builder.ExpectSegment(
HasEncryptionData,
std::make_tuple(GURL("http://localhost/enc.key"), XKeyTagMethod::kAES128,
- XKeyTagKeyFormat::kIdentity, std::make_tuple(0, 4)));
+ XKeyTagKeyFormat::kIdentity, std::make_tuple(0, 4),
+ MediaSegment::EncryptionData::KeyLocation::kSafeOrigin));
builder.ExpectOk();
builder.AppendLine(
@@ -1541,7 +1544,8 @@
HasEncryptionData,
std::make_tuple(GURL("http://localhost/enc.key"),
XKeyTagMethod::kSampleAES, XKeyTagKeyFormat::kIdentity,
- std::make_tuple(0, 5)));
+ std::make_tuple(0, 5),
+ MediaSegment::EncryptionData::KeyLocation::kSafeOrigin));
builder.ExpectOk();
builder.AppendLine(
@@ -1556,7 +1560,74 @@
HasEncryptionData,
std::make_tuple(GURL("http://localhost/enc.key"),
XKeyTagMethod::kSampleAESCTR, XKeyTagKeyFormat::kIdentity,
- std::make_tuple(0, 6)));
+ std::make_tuple(0, 6),
+ MediaSegment::EncryptionData::KeyLocation::kSafeOrigin));
+ builder.ExpectOk();
+
+ builder.AppendLine(
+ "#EXT-X-KEY:METHOD=SAMPLE-AES-CTR,URI=\"https://example.com/"
+ "key\",KEYFORMAT="
+ "\"identity\"");
+ builder.AppendLine("#EXTINF:1.60000,");
+ builder.AppendLine("data06.ts");
+ builder.ExpectAdditionalSegment();
+ builder.ExpectSegment(HasUri, GURL("http://localhost/data06.ts"));
+ builder.ExpectSegment(HasMediaSequenceNumber, 7);
+ builder.ExpectSegment(
+ HasEncryptionData,
+ std::make_tuple(
+ GURL("https://example.com/key"), XKeyTagMethod::kSampleAESCTR,
+ XKeyTagKeyFormat::kIdentity, std::make_tuple(0, 7),
+ MediaSegment::EncryptionData::KeyLocation::kUnsafeOrigin));
+ builder.ExpectOk();
+
+ builder.AppendLine(
+ "#EXT-X-KEY:METHOD=SAMPLE-AES-CTR,URI=\"data:text/"
+ "plain;base64,SGVsbG8sIFdvcmxkIQ==\",KEYFORMAT="
+ "\"identity\"");
+ builder.AppendLine("#EXTINF:1.60000,");
+ builder.AppendLine("data06.ts");
+ builder.ExpectAdditionalSegment();
+ builder.ExpectSegment(HasUri, GURL("http://localhost/data06.ts"));
+ builder.ExpectSegment(HasMediaSequenceNumber, 8);
+ builder.ExpectSegment(
+ HasEncryptionData,
+ std::make_tuple(GURL("data:text/plain;base64,SGVsbG8sIFdvcmxkIQ=="),
+ XKeyTagMethod::kSampleAESCTR, XKeyTagKeyFormat::kIdentity,
+ std::make_tuple(0, 8),
+ MediaSegment::EncryptionData::KeyLocation::kSafeOrigin));
+ builder.ExpectOk();
+
+ builder.AppendLine(
+ "#EXT-X-KEY:METHOD=SAMPLE-AES-CTR,URI=\"//keyhost/key\","
+ "KEYFORMAT=\"identity\"");
+ builder.AppendLine("#EXTINF:1.60000,");
+ builder.AppendLine("data07.ts");
+ builder.ExpectAdditionalSegment();
+ builder.ExpectSegment(HasUri, GURL("http://localhost/data07.ts"));
+ builder.ExpectSegment(HasMediaSequenceNumber, 9);
+ builder.ExpectSegment(
+ HasEncryptionData,
+ std::make_tuple(
+ GURL("http://keyhost/key"), XKeyTagMethod::kSampleAESCTR,
+ XKeyTagKeyFormat::kIdentity, std::make_tuple(0, 9),
+ MediaSegment::EncryptionData::KeyLocation::kUnsafeOrigin));
+ builder.ExpectOk();
+
+ builder.AppendLine(
+ "#EXT-X-KEY:METHOD=SAMPLE-AES-CTR,URI=\"/keypath/key\","
+ "KEYFORMAT=\"identity\"");
+ builder.AppendLine("#EXTINF:1.60000,");
+ builder.AppendLine("data08.ts");
+ builder.ExpectAdditionalSegment();
+ builder.ExpectSegment(HasUri, GURL("http://localhost/data08.ts"));
+ builder.ExpectSegment(HasMediaSequenceNumber, 10);
+ builder.ExpectSegment(
+ HasEncryptionData,
+ std::make_tuple(GURL("http://localhost/keypath/key"),
+ XKeyTagMethod::kSampleAESCTR, XKeyTagKeyFormat::kIdentity,
+ std::make_tuple(0, 10),
+ MediaSegment::EncryptionData::KeyLocation::kSafeOrigin));
builder.ExpectOk();
}
... (truncated)
Original Bug Report
Cross-origin response length and verification oracle via HLS EXT-X-KEY
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: The HLS demuxer fetches EXT-X-KEY URIs with credentials by default when the crossorigin attribute is omitted from a video element, processing the opaque response as an AES key. This creates observable side channels through decryption error events, potentially allowing attackers to leak the length of cross-origin responses or verify guesses of 16/32-byte secrets.
Affected files:
media/formats/hls/media_playlist.ccmedia/filters/hls_network_access_impl.ccmedia/formats/hls/media_segment.ccthird_party/blink/renderer/platform/media/web_media_player_impl.cc
Estimated timestamp from git blame: 2025-09-03
The built-in HLS demuxer in Chromium contains a potential vulnerability that allows for cross-origin information leaks. When processing an #EXT-X-KEY tag in an HLS manifest, the demuxer resolves the URI and fetches the key using the <video> element’s CORS mode.
If the crossorigin attribute is not specified on the video element, the mode defaults to RequestMode::kNoCors with CredentialsMode::kInclude. Consequently, requests for the AES key will include SameSite=None cookies. If the response is not blocked by Opaque Response Blocking (ORB)—for instance, if it lacks a protected Content-Type and nosniff header—the raw bytes of the opaque response body are passed directly to the C++ media stack and used as an AES key (MediaSegment::EncryptionData::ImportKey).
This behavior creates two potential side-channel oracles observable via the <video> element’s JavaScript error event:
- Length Oracle: In
media/formats/hls/media_segment.cc,KeyIsValidSize()requires the key to be exactly 16 or 32 bytes. If the cross-origin response is any other length, the demuxer immediately fails (kFailedToDecryptSegment), triggering an observable error event. - Verification Oracle: If the length is exactly 16 or 32 bytes, the implementation attempts to decrypt the segment using
crypto::aes_cbc::Decrypt. This function relies on BoringSSL’sEVP_DecryptFinal_ex2, which enforces PKCS#7 padding validation. Because the attacker controls the ciphertext (the segment data) and the IV, they can craft a ciphertext that decrypts to valid PKCS#7 padding only if their guessed key matches the actual cross-origin response. An incorrect guess results in invalid padding ~99.6% of the time, throwing a decryption error and an observable<video>error event. A correct guess succeeds the padding check, altering the observable state.
Potential Attacker Steps
Note: These are suggested steps based on static code analysis; our tooling agent does not yet have the ability to run or verify exploit code.
- An attacker creates a webpage hosting a
<video>element without acrossoriginattribute. - The video source points to an attacker-controlled HLS playlist (
.m3u8). - The playlist contains an
#EXT-X-KEYtag pointing to a target cross-origin endpoint (e.g.,http://victim.com/secret). - The playlist points to an attacker-crafted encrypted media segment.
- The attacker uses JavaScript to monitor the
<video>element forerrorevents. - By repeatedly changing the video source to use different crafted ciphertexts, the attacker can use the padding validation as a 1-bit verification oracle to systematically brute-force 16- or 32-byte secrets stored on the cross-origin endpoint.
Suggested Fix
Cryptographic key material should never be sourced from an opaque, cross-origin response.
To fix this, the HLS implementation should verify that the response used for #EXT-X-KEY does not taint the origin. In media/filters/hls_network_access_impl.cc (OnKeyFetch), check if the HlsDataSourceStream was flagged with WouldTaintOrigin(). If it was, the key fetch should be rejected and an error propagated. Alternatively, enforce that #EXT-X-KEY fetches strictly require a successful CORS preflight/response, regardless of the media element’s overall crossorigin state.
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.