CVE-2026-106398
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fmedia/filters/hls_data_source_provider_impl_unittest.cc |
modified | |
TEST_Fthird_party/blink/renderer/platform/media/multi_buffer_data_source_unittest.cc |
modified |
Files Changed
media/filters/hls_test_helpers.ccthird_party/blink/renderer/platform/media/multi_buffer_data_source_unittest.ccmedia/base/data_source.hthird_party/blink/renderer/platform/media/multi_buffer_data_source.hmedia/base/data_source.ccthird_party/blink/renderer/platform/media/url_index.hmedia/filters/hls_data_source_provider_impl.ccthird_party/blink/renderer/platform/media/multi_buffer_data_source.ccmedia/filters/hls_test_helpers.hmedia/filters/hls_data_source_provider_impl_unittest.cc
Patch
From 91c9bdd8ef00d117b26921788ace7310694ba51a Mon Sep 17 00:00:00 2001
From: Ted Meyer <tmathmeyer@chromium.org>
Date: Wed, 02 Sep 2026 13:54:24 -0700
Subject: [PATCH] [Media] Propagate data origin to MultiBufferDataSource
There are a few ways that a URL can change between request and response,
including redirects and service worker interceptions.
GetUrlAfterRedirects only handles cases where the original request is
redirected server-side by a header & status code. If a worker intercepts
the request and replaces it with a response from either somewhere else
or synthetically creates one, that information can also be useful for
making security decisions. It wasn't being propagated, so this CL adds a
way for that to happen, and a use case for it in HLS.
Bug: 543082212
Change-Id: I41b3dd73f23fa2d2211dcd3f37b1e7b83e36b250
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8342168
Reviewed-by: Dale Curtis <dalecurtis@chromium.org>
Commit-Queue: Ted (Chromium) Meyer <tmathmeyer@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1691072}
---
diff --git a/media/base/data_source.cc b/media/base/data_source.cc
index a918ed0f..04d377b 100644
--- a/media/base/data_source.cc
+++ b/media/base/data_source.cc
@@ -28,6 +28,10 @@
return GURL();
}
+GURL DataSource::GetUrlDataOrigin() const {
+ return GetUrlAfterRedirects();
+}
+
bool DataSource::DidRedirect() const {
return false;
}
diff --git a/media/base/data_source.h b/media/base/data_source.h
index c9fc04e6..652bbd9 100644
--- a/media/base/data_source.h
+++ b/media/base/data_source.h
@@ -124,6 +124,10 @@
// an empty GURL.
virtual GURL GetUrlAfterRedirects() const;
+ // The final URL from which data was loaded. If the request was intercepted
+ // by a service worker, this may differ from GetUrlAfterRedirects.
+ virtual GURL GetUrlDataOrigin() const;
+
// Stops any outstanding speculative loading. Active and future Read() calls
// will not be stopped. OnMediaIsPlaying() or OnMediaPlaybackRateChanged()
// with a rate > 0 will reset this flag.
diff --git a/media/filters/hls_data_source_provider_impl.cc b/media/filters/hls_data_source_provider_impl.cc
index 8bad55a..e774dbe 100644
--- a/media/filters/hls_data_source_provider_impl.cc
+++ b/media/filters/hls_data_source_provider_impl.cc
@@ -195,7 +195,7 @@
if (it->second->DidRedirect()) {
stream->set_did_redirect();
}
- const auto& response_uri = it->second->GetUrlAfterRedirects();
+ const auto& response_uri = it->second->GetUrlDataOrigin();
if (!response_uri.is_empty()) {
if (!response_uri.SchemeIs("data")) {
stream->TrackOrigin(url::Origin::Create(response_uri));
diff --git a/media/filters/hls_test_helpers.cc b/media/filters/hls_test_helpers.cc
index 16ae82c..52c4cbb 100644
--- a/media/filters/hls_test_helpers.cc
+++ b/media/filters/hls_test_helpers.cc
@@ -145,6 +145,7 @@
.WillByDefault(base::test::RunOnceCallback<0>(true));
ON_CALL(*mock, Stop()).WillByDefault(testing::Return());
ON_CALL(*mock, GetUrlAfterRedirects()).WillByDefault(testing::Return(uri));
+ ON_CALL(*mock, GetUrlDataOrigin()).WillByDefault(testing::Return(uri));
ON_CALL(*mock, DidRedirect()).WillByDefault(testing::Return(false));
ON_CALL(*mock, WouldTaintOrigin()).WillByDefault(testing::Return(false));
}
@@ -155,6 +156,7 @@
.WillByDefault(base::test::RunOnceCallback<0>(false));
EXPECT_CALL(*mock, Stop()).Times(0);
EXPECT_CALL(*mock, GetUrlAfterRedirects()).Times(0);
+ EXPECT_CALL(*mock, GetUrlDataOrigin()).Times(0);
EXPECT_CALL(*mock, DidRedirect()).Times(0);
ON_CALL(*mock, WouldTaintOrigin()).WillByDefault(testing::Return(false));
}
@@ -167,6 +169,7 @@
ON_CALL(*mock, Stop()).WillByDefault(testing::Return());
ON_CALL(*mock, GetUrlAfterRedirects())
.WillByDefault(testing::Return(target_uri));
+ ON_CALL(*mock, GetUrlDataOrigin()).WillByDefault(testing::Return(target_uri));
ON_CALL(*mock, DidRedirect()).WillByDefault(testing::Return(true));
ON_CALL(*mock, WouldTaintOrigin()).WillByDefault(testing::Return(false));
}
diff --git a/media/filters/hls_test_helpers.h b/media/filters/hls_test_helpers.h
index b763cdc..e8e69e7 100644
--- a/media/filters/hls_test_helpers.h
+++ b/media/filters/hls_test_helpers.h
@@ -49,6 +49,7 @@
MOCK_METHOD(void, SetPreload, (DataSource::Preload preload), (override));
MOCK_METHOD(bool, DidRedirect, (), (const, override));
MOCK_METHOD(GURL, GetUrlAfterRedirects, (), (const, override));
+ MOCK_METHOD(GURL, GetUrlDataOrigin, (), (const, override));
MOCK_METHOD(void, StopPreloading, (), (override));
MOCK_METHOD(void,
OnMediaPlaybackRateChanged,
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 bf02001..28a0d9a 100644
--- a/third_party/blink/renderer/platform/media/multi_buffer_data_source.cc
+++ b/third_party/blink/renderer/platform/media/multi_buffer_data_source.cc
@@ -395,6 +395,13 @@
return GURL(url_data_->url());
}
+GURL MultiBufferDataSource::GetUrlDataOrigin() const {
+ if (url_data_->data_origin().has_value()) {
+ return GURL(*url_data_->data_origin());
+ }
+ return GetUrlAfterRedirects();
+}
+
void MultiBufferDataSource::Read(int64_t position,
base::span<uint8_t> data,
media::DataSource::ReadCB read_cb) {
diff --git a/third_party/blink/renderer/platform/media/multi_buffer_data_source.h b/third_party/blink/renderer/platform/media/multi_buffer_data_source.h
index 75260f0..ad5ca846 100644
--- a/third_party/blink/renderer/platform/media/multi_buffer_data_source.h
+++ b/third_party/blink/renderer/platform/media/multi_buffer_data_source.h
@@ -145,6 +145,7 @@
bool DidRedirect() const override { return did_redirect_; }
GURL GetUrlAfterRedirects() const override;
+ GURL GetUrlDataOrigin() const override;
// media::DataSource implementation.
// Called from demuxer thread.
diff --git a/third_party/blink/renderer/platform/media/url_index.h b/third_party/blink/renderer/platform/media/url_index.h
index df99163f..1bafe49 100644
--- a/third_party/blink/renderer/platform/media/url_index.h
+++ b/third_party/blink/renderer/platform/media/url_index.h
@@ -93,6 +93,8 @@
// Accessors
const KURL& url() const { return url_; }
+ const std::optional<KURL>& data_origin() const { return data_origin_; }
+
// Cross-origin access mode
CorsMode cors_mode() const { return cors_mode_; }
diff --git a/media/filters/hls_data_source_provider_impl_unittest.cc b/media/filters/hls_data_source_provider_impl_unittest.cc
index 45ed7b2..c38e47e 100644
--- a/media/filters/hls_data_source_provider_impl_unittest.cc
+++ b/media/filters/hls_data_source_provider_impl_unittest.cc
@@ -169,7 +169,7 @@
.WillOnce([&read_cb, &write_out](auto* data_source, const auto&, ...) {
EXPECT_CALL(*data_source, Initialize)
.WillOnce(RunOnceCallback<0>(true));
- EXPECT_CALL(*data_source, GetUrlAfterRedirects())
+ EXPECT_CALL(*data_source, GetUrlDataOrigin())
.WillOnce(testing::Return(GURL("https://example.com")));
EXPECT_CALL(*data_source, Stop()).Times(0);
EXPECT_CALL(*data_source, Abort()).Times(0);
@@ -313,5 +313,37 @@
ASSERT_TRUE(has_error);
}
+TEST_F(HlsDataSourceProviderImplUnittest, ReadSegmentWithDifferentDataOrigin) {
+ HlsDataSourceProvider::UrlDataSegment segment(GURL("http://evil.com"),
+ std::nullopt);
+
+ const GURL url("http://evil.com");
+ EXPECT_CALL(*factory_, Setup(_, url, _, _))
+ .WillOnce([](MockDataSource* mock, const GURL& uri, ...) {
+ MockDataSourceFactory::ConfigureAsSuccess(mock, uri);
+ ON_CALL(*mock, GetUrlAfterRedirects())
+ .WillByDefault(testing::Return(GURL("http://evil.com")));
+ ON_CALL(*mock, GetUrlDataOrigin())
+ .WillByDefault(testing::Return(GURL("http://innocent.com")));
+ EXPECT_CALL(*mock, Read(0, SpanSizeEq(16384), _))
+ .WillOnce(base::test::RunOnceCallback<2>(4096));
+ });
+
+ std::unique_ptr<HlsDataSourceStream> first_read;
+ impl_->ReadFromUrl(std::move(segment),
+ base::BindOnce(
+ [](std::unique_ptr<HlsDataSourceStream>* extract,
+ HlsDataSourceProvider::ReadResult result) {
+ *extract = std::move(result).value();
+ },
+ &first_read));
+ task_environment_.RunUntilIdle();
+ ASSERT_NE(first_read, nullptr);
+ ASSERT_TRUE(first_read->CanReadMore());
+ ASSERT_FALSE(first_read->RequiresInit());
+ ASSERT_EQ(first_read->SecurityInfo().response_origins.size(), 1u);
+ ASSERT_EQ(*first_read->SecurityInfo().response_origins.begin(),
+ url::Origin::Create(GURL("http://innocent.com")));
+}
} // namespace media
diff --git a/third_party/blink/renderer/platform/media/multi_buffer_data_source_unittest.cc b/third_party/blink/renderer/platform/media/multi_buffer_data_source_unittest.cc
index a1c138c..03aabf4 100644
--- a/third_party/blink/renderer/platform/media/multi_buffer_data_source_unittest.cc
+++ b/third_party/blink/renderer/platform/media/multi_buffer_data_source_unittest.cc
@@ -649,6 +649,29 @@
Stop();
}
+TEST_F(MultiBufferDataSourceTest, GetUrlDataOrigin_NoDataOrigin) {
+ InitializeWith200Response();
+ // Without a data origin, it should fallback to url() via
+ // GetUrlAfterRedirects().
+ EXPECT_EQ(data_source_->GetUrlDataOrigin(), GURL(kHttpUrl));
+ Stop();
+}
+
+TEST_F(MultiBufferDataSourceTest, GetUrlDataOrigin_WithDataOrigin) {
+ Initialize(kHttpUrl, true);
+
+ EXPECT_CALL(host_, SetTotalBytes(response_generator_->content_length()));
+ WebURLResponse response = response_generator_->Generate200();
+ response.SetCurrentRequestUrl(KURL(kHttpDifferentOriginUrl));
+ Respond(response);
+
+ EXPECT_CALL(host_, AddBufferedByteRange(0, kDataSize));
+ ReceiveData(kDataSize);
+
+ EXPECT_EQ(data_source_->GetUrlDataOrigin(), GURL(kHttpDifferentOriginUrl));
+ Stop();
+}
+
TEST_F(MultiBufferDataSourceTest, Http_AbortWhileReading) {
InitializeWith206Response();
Regression Test / PoC
diff --git a/media/filters/hls_data_source_provider_impl_unittest.cc b/media/filters/hls_data_source_provider_impl_unittest.cc
index 45ed7b2..c38e47e 100644
--- a/media/filters/hls_data_source_provider_impl_unittest.cc
+++ b/media/filters/hls_data_source_provider_impl_unittest.cc
@@ -169,7 +169,7 @@
.WillOnce([&read_cb, &write_out](auto* data_source, const auto&, ...) {
EXPECT_CALL(*data_source, Initialize)
.WillOnce(RunOnceCallback<0>(true));
- EXPECT_CALL(*data_source, GetUrlAfterRedirects())
+ EXPECT_CALL(*data_source, GetUrlDataOrigin())
.WillOnce(testing::Return(GURL("https://example.com")));
EXPECT_CALL(*data_source, Stop()).Times(0);
EXPECT_CALL(*data_source, Abort()).Times(0);
@@ -313,5 +313,37 @@
ASSERT_TRUE(has_error);
}
+TEST_F(HlsDataSourceProviderImplUnittest, ReadSegmentWithDifferentDataOrigin) {
+ HlsDataSourceProvider::UrlDataSegment segment(GURL("http://evil.com"),
+ std::nullopt);
+
+ const GURL url("http://evil.com");
+ EXPECT_CALL(*factory_, Setup(_, url, _, _))
+ .WillOnce([](MockDataSource* mock, const GURL& uri, ...) {
+ MockDataSourceFactory::ConfigureAsSuccess(mock, uri);
+ ON_CALL(*mock, GetUrlAfterRedirects())
+ .WillByDefault(testing::Return(GURL("http://evil.com")));
+ ON_CALL(*mock, GetUrlDataOrigin())
+ .WillByDefault(testing::Return(GURL("http://innocent.com")));
+ EXPECT_CALL(*mock, Read(0, SpanSizeEq(16384), _))
+ .WillOnce(base::test::RunOnceCallback<2>(4096));
+ });
+
+ std::unique_ptr<HlsDataSourceStream> first_read;
+ impl_->ReadFromUrl(std::move(segment),
+ base::BindOnce(
+ [](std::unique_ptr<HlsDataSourceStream>* extract,
+ HlsDataSourceProvider::ReadResult result) {
+ *extract = std::move(result).value();
+ },
+ &first_read));
+ task_environment_.RunUntilIdle();
+ ASSERT_NE(first_read, nullptr);
+ ASSERT_TRUE(first_read->CanReadMore());
+ ASSERT_FALSE(first_read->RequiresInit());
+ ASSERT_EQ(first_read->SecurityInfo().response_origins.size(), 1u);
+ ASSERT_EQ(*first_read->SecurityInfo().response_origins.begin(),
+ url::Origin::Create(GURL("http://innocent.com")));
+}
} // namespace media
diff --git a/third_party/blink/renderer/platform/media/multi_buffer_data_source_unittest.cc b/third_party/blink/renderer/platform/media/multi_buffer_data_source_unittest.cc
index a1c138c..03aabf4 100644
--- a/third_party/blink/renderer/platform/media/multi_buffer_data_source_unittest.cc
+++ b/third_party/blink/renderer/platform/media/multi_buffer_data_source_unittest.cc
@@ -649,6 +649,29 @@
Stop();
}
+TEST_F(MultiBufferDataSourceTest, GetUrlDataOrigin_NoDataOrigin) {
+ InitializeWith200Response();
+ // Without a data origin, it should fallback to url() via
+ // GetUrlAfterRedirects().
+ EXPECT_EQ(data_source_->GetUrlDataOrigin(), GURL(kHttpUrl));
+ Stop();
+}
+
+TEST_F(MultiBufferDataSourceTest, GetUrlDataOrigin_WithDataOrigin) {
+ Initialize(kHttpUrl, true);
+
+ EXPECT_CALL(host_, SetTotalBytes(response_generator_->content_length()));
+ WebURLResponse response = response_generator_->Generate200();
+ response.SetCurrentRequestUrl(KURL(kHttpDifferentOriginUrl));
+ Respond(response);
+
+ EXPECT_CALL(host_, AddBufferedByteRange(0, kDataSize));
+ ReceiveData(kDataSize);
+
+ EXPECT_EQ(data_source_->GetUrlDataOrigin(), GURL(kHttpDifferentOriginUrl));
+ Stop();
+}
+
TEST_F(MultiBufferDataSourceTest, Http_AbortWhileReading) {
InitializeWith206Response();
Original Bug Report
HLS accepts a Service Worker-proxied opaque cross-origin response as an AES key, enabling authenticated offline dictionary recovery
Report description
HLS accepts a Service Worker-proxied opaque cross-origin response as an AES key, enabling authenticated offline dictionary recovery
Bug location
Where do you want to report your vulnerability?
Chrome VRP – Report security issues affecting the Chrome browser. See program rules
Which URL (or repository) have you found the vulnerability in?
https://chromium.googlesource.com/chromium/src/+/HEAD/media/filters/hls_data_source_provider_impl.cc https://chromium.googlesource.com/chromium/src/+/HEAD/media/formats/hls/security_metadata.cc https://chromium.googlesource.com/chromium/src/+/HEAD/third_party/blink/renderer/platform/media/multi_buffer_data_source.cc
The problem
Please describe the technical details of the vulnerability
Chrome blocks an HLS manifest from using an AES key obtained through a direct
cross-origin redirect. A same-origin Service Worker can bypass that protection
by following the same redirect with fetch(event.request) and returning the
resulting opaque response.
HLS -> /key-redirect -> 302 cross-origin -> BLOCKED
HLS -> /key-redirect -> Service Worker fetch(event.request)
-> same 302 cross-origin -> PLAYED
Both paths reach the same final URL, carry Range: bytes=0-, and receive the
same 16-byte response. The security-relevant difference is whether the media
loader observes the redirect or the Service Worker follows it before returning
the response.
The minimal worker code is:
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
if (url.pathname === '/key-redirect' &&
url.searchParams.get('via') === 'sw') {
event.respondWith(fetch(event.request));
}
});
The server returns the same redirect in the direct control and Service Worker cases:
HTTP/1.1 302 Found
Location: https://target.example/secret
The four-case PoC produces:
same_origin PLAYED
direct_redirect BLOCKED
sw_redirect_known PLAYED
sw_redirect_wrong BLOCKED
sw_redirect_wrong follows the same worker and redirect path but receives a
different 16-byte value. It fails to decrypt the media. This establishes a
content-equality signal, not merely that the cross-origin request completed.
Authenticated cached-response impact
The Service Worker can store the opaque response in Cache Storage and return a fresh cached clone for later HLS key requests. For each candidate value, the attacker supplies valid media encrypted under that candidate. Playback succeeds only when the candidate equals the hidden cached response.
The attached impact PoC uses an HTTPS endpoint that returns HTTP 403 unless the
request carries its normal SameSite=None; Secure session cookie. The cookie is
established through a first-party HTTPS navigation and an ordinary Set-Cookie
response. The authenticated endpoint returns exactly:
PIN=1337;.......
Observed on Chrome Stable 151.0.7922.76:
same-origin AES key PLAYED
direct authenticated cross-origin key BLOCKED
opaque response stored in Cache Storage YES
cached correct response PLAYED
cached equal-length wrong candidate BLOCKED
recovered dictionary value 1337
target requests during enumeration 0
1337 is the 1,338th member of the 0000-9999 dictionary. The PoC evaluates
48 candidates concurrently, so its successful batch finishes at candidate
1343 and records 1,344 candidate HLS loads. Setup and control loads are not
included in that number.
The reference run recovered the value in 19.463 seconds. A separate worst-case
run placed the match at 9999, evaluated all 10,000 candidates in 143.795
seconds, and made no additional request to the target during enumeration.
This is whole-response recovery when the complete response belongs to a practical candidate set. It is not arbitrary byte extraction or brute force of a high-entropy AES key.
Root cause
Relevant Chromium code:
https://chromium.googlesource.com/chromium/src/+/HEAD/media/filters/hls_data_source_provider_impl.cc
https://chromium.googlesource.com/chromium/src/+/HEAD/media/formats/hls/security_metadata.cc
https://chromium.googlesource.com/chromium/src/+/HEAD/third_party/blink/renderer/platform/media/multi_buffer_data_source.cc
HlsDataSourceProviderImpl::DataSourceInitialized() records the URL returned by
GetUrlAfterRedirects(). For MultiBufferDataSource, that URL is obtained from
the outer UrlData:
GURL MultiBufferDataSource::GetUrlAfterRedirects() const {
return GURL(url_data_->url());
}
When the Service Worker follows the redirect, this remains the same-origin
/key-redirect request URL. It does not represent the final origin supplying
the opaque body. Consequently, HLS security metadata contains only the manifest
origin and accepts the key:
actual key bytes: cross-origin target
recorded response URL: same-origin Service Worker URL
recorded origin set: manifest origin
result: key accepted
Chrome therefore consumes the opaque cross-origin response body as an HLS AES key while treating it as though it came from the manifest origin.
Relationship to issue 501254242
Public issue 501254242 fixed a direct HTTP-redirect bypass of the HLS key-origin restriction:
https://issues.chromium.org/issues/501254242
That fix rejects redirects observed by the HLS media data source. This report
uses a different transition: the Service Worker follows the redirect inside
fetch(event.request), so the outer media data source receives a same-origin
Service Worker response and does not retain the redirect or final-origin state.
The PoC’s direct-redirect case is blocked on the tested builds, confirming that the previous fix is present. The Service Worker case remains accepted.
Source-history introduction range
The first Chromium commit at which this equality oracle can exist is the commit that added native AES-128 HLS segment decryption using a fetched playlist key:
last unaffected: 0b9dd1c9d4de4159d9b5512e3e9e9a3ae6e9922e
first affected: 1b9326aa2ff0db894fe215a10cb89899665f8020
first affected date: 2024-07-25
commit position: refs/heads/main@{#1333263}
subject: Decrypt AES-128 content in HLS
https://chromium.googlesource.com/chromium/src/+/1b9326aa2ff0db894fe215a10cb89899665f8020
The parent has no native path that passes a fetched HLS key to the segment
decryptor. The first affected commit adds the bridge used by this PoC: fetched
EncryptionData::GetKey() bytes influence whether attacker-supplied encrypted
media can be parsed and played.
The later hardening history explains the remaining gap:
2026-04-02 db46e35db798aeee6cea733fcb31f5d8680d6416 Restrict origins for HLS keys
2026-04-16 a8db6f5f733cfec577972326fa249a8551dbc394 Check for redirection when determining key safety
2026-06-11 cac536c341385184852d2508b92e461deb7dfd72 Track security origins included in streams
The April fix records a redirect only when the media data source observes it.
The June change records GetUrlAfterRedirects(), which still yields the outer
same-origin URL for the Service Worker response.
Reproduction
Attach these files individually:
hls_sw_opaque_key_poc.py
encrypted_small.ts
hls_authenticated_dictionary_poc.py
plain_small.ts
hls_evidence_summary.json
Both Python files are server-only PoCs and do not launch or control Chrome.
Minimal four-case reproduction
- Run
python3 hls_sw_opaque_key_poc.py. - Start the tested Chrome build with a fresh profile and no extensions or enterprise policies.
- Open
http://localhost:18340/. - Do not interact with the page. It registers the Service Worker, reloads once to become controlled, and executes all four cases automatically.
- Observe
VULNERABLEand the four-case matrix shown above.
The minimal script uses only Python’s standard library. encrypted_small.ts is
a harmless 1.16-second encrypted AAC transport stream.
Authenticated dictionary reproduction
This PoC additionally requires Python’s cryptography package and OpenSSL.
- Run
python3 hls_authenticated_dictionary_poc.py. - In a fresh normal Chrome profile, open the printed HTTPS
/set-cookieURL. Accept the warning for the one-day self-signed loopback certificate. This represents an ordinary first-party authenticated visit. - Follow the displayed link to the printed
http://localhostattacker URL. - No further interaction is required. The page executes the controls and the complete four-digit enumeration.
- Observe
VULNERABLE,found: 1337,dictionaryOrdinal: 1338, andcandidateLoads: 1344.targetBeforeSearchandtargetAfterSearchremain equal; both were4in the reference run.
plain_small.ts is a harmless 1.16-second AAC transport stream used to create
candidate encrypted segments in memory.
Expected behavior
The Service Worker response should be rejected like the direct redirect because its opaque body was supplied by a different origin.
Actual behavior
Chrome imports the opaque cross-origin body as the HLS AES key and reveals whether it decrypts attacker-supplied media.
Impact analysis
An attacker who controls a web origin can use Chrome’s HLS decryptor to test the
complete contents of a cross-origin response that JavaScript is not allowed to
read. Ordinarily, a permitted no-cors request exposes only an opaque response:
script cannot inspect its status, headers, or body. This bug passes those hidden
body bytes to the HLS AES implementation and exposes whether they exactly equal
an attacker-chosen candidate.
Cache Storage makes the oracle persistent and offline. An already-authenticated victim visits the attacker’s Service-Worker-controlled page. The worker makes one credentialed request permitted by ordinary cookie policy, retains the opaque response, and returns cached clones for the entire candidate search. The target receives no request per candidate and cannot observe or rate-limit the offline testing.
The attached PoC demonstrates the disclosure directly:
before exploitation: attacker cannot read the authenticated response body
after one opaque fetch: body remains unreadable to JavaScript
after offline HLS tests: complete value PIN=1337;....... recovered
target requests during all 1,344 candidate tests: 0
No permission prompt or interaction is required after the authenticated victim opens the attacker page. The attacker gains the complete SOP-protected response when it has the required AES-key length and belongs to a practical dictionary.
Preconditions and limits:
- Normal browser rules must permit the no-CORS request and any required credentials.
- The target must not block the response using CORP.
- The complete target response must be 16 bytes and belong to a practical candidate set.
- The attack does not bypass SameSite, third-party-cookie, CORP, or Local Network Access policy.
- It does not make brute force of a high-entropy 128-bit value practical and does not provide arbitrary byte-by-byte extraction.
These constraints limit which endpoints qualify, but for a qualifying low-entropy authenticated response the PoC recovers the entire protected body, not merely its size or whether a request occurred.
The cause
What version of Chrome have you found the security issue in?
151.0.7922.76 Stable Revision: 031bfd60f544af71d8dd7246970d1af3e1acf3ad-refs/branch-heads/7922_47@{#18} Ubuntu 24.04.4 LTS x86_64 Fresh profile; no extensions or enterprise policies 153.0.7990.0 Canary Four-case redirect matrix reproduced
Is the security issue related to a crash?
No, it is not related to a crash.
Choose the type of vulnerability
Information Leak
How would you like to be publicly acknowledged for your report?
caveeroo
- http://localhost:18340/
- https://bughunters.google.com/about/rules/5745167867576320/chrome-vulnerability-reward-program-rules
- https://chromium.googlesource.com/chromium/src/+/1b9326aa2ff0db894fe215a10cb89899665f8020
- https://chromium.googlesource.com/chromium/src/+/HEAD/media/filters/hls_data_source_provider_impl.cc
- https://chromium.googlesource.com/chromium/src/+/HEAD/media/formats/hls/security_metadata.cc
- https://chromium.googlesource.com/chromium/src/+/HEAD/third_party/blink/renderer/platform/media/multi_buffer_data_source.cc
- https://issuetracker.google.com/issues/501254242