CVE-2026-11134
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/platform/media/resource_multi_buffer_data_provider.cc |
modified | |
TEST_Fthird_party/blink/renderer/platform/media/resource_multi_buffer_data_provider_unittest.cc |
modified | |
TEST_Fthird_party/blink/renderer/platform/media/url_index_unittest.cc |
modified |
Files Changed
third_party/blink/renderer/platform/media/resource_multi_buffer_data_provider.ccthird_party/blink/renderer/platform/media/resource_multi_buffer_data_provider_unittest.ccthird_party/blink/renderer/platform/media/url_index.ccthird_party/blink/renderer/platform/media/url_index_unittest.cc
Patch
From 5a92a8c2fe282cb330e6ca8a05f06b24aa310bfe Mon Sep 17 00:00:00 2001 From: Dale Curtis <[email protected]> Date: Thu, 16 Apr 2026 11:58:05 -0700 Subject: [PATCH] Improve MultiBuffer CORS access checks for redirects This fixes two issues with redirects within the MultiBuffer code: * ResourceMultiBufferDataProvider should fail more redirects. * Merging data between two MultiBuffers must preserve tainting. R=tmathmeyer Fixed: 501640084 Change-Id: I6dea9bc5a3209a1709d093a6741e5f87e1745dfd Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7766678 Reviewed-by: Ted (Chromium) Meyer <[email protected]> Commit-Queue: Ted (Chromium) Meyer <[email protected]> Commit-Queue: Dale Curtis <[email protected]> Auto-Submit: Dale Curtis <[email protected]> Cr-Commit-Position: refs/heads/main@{#1616037} --- diff --git a/third_party/blink/renderer/platform/media/resource_multi_buffer_data_provider.cc b/third_party/blink/renderer/platform/media/resource_multi_buffer_data_provider.cc index 9d7baea..5af3708 100644 --- a/third_party/blink/renderer/platform/media/resource_multi_buffer_data_provider.cc +++ b/third_party/blink/renderer/platform/media/resource_multi_buffer_data_provider.cc @@ -188,7 +188,8 @@ // This test is vital for security! if (cors_mode_ == UrlData::CORS_UNSPECIFIED) { // We allow the redirect if the origin is the same. - if (!SecurityOrigin::AreSameOrigin(original_url_, redirects_to_)) { + if (!SecurityOrigin::AreSameOrigin(original_url_, redirects_to_) || + !SecurityOrigin::AreSameOrigin(url_data_->url(), redirects_to_)) { // We also allow the redirect if we don't have any data in the // cache, as that means that no dangerous data mixing can occur. if (url_data_->multibuffer()->map().empty() && fifo_.empty()) diff --git a/third_party/blink/renderer/platform/media/resource_multi_buffer_data_provider_unittest.cc b/third_party/blink/renderer/platform/media/resource_multi_buffer_data_provider_unittest.cc index 0318e17..6431a01 100644 --- a/third_party/blink/renderer/platform/media/resource_multi_buffer_data_provider_unittest.cc +++ b/third_party/blink/renderer/platform/media/resource_multi_buffer_data_provider_unittest.cc @@ -14,6 +14,7 @@ #include "base/containers/heap_array.h" #include "base/format_macros.h" +#include "base/functional/callback_helpers.h" #include "base/memory/raw_ptr.h" #include "base/memory/scoped_refptr.h" #include "base/run_loop.h" @@ -34,6 +35,7 @@ #include "third_party/blink/public/platform/web_url_error.h" #include "third_party/blink/public/platform/web_url_request.h" #include "third_party/blink/public/platform/web_url_response.h" +#include "third_party/blink/renderer/platform/media/multi_buffer_reader.h" #include "third_party/blink/renderer/platform/media/testing/mock_resource_fetch_context.h" #include "third_party/blink/renderer/platform/media/testing/mock_web_associated_url_loader.h" #include "third_party/blink/renderer/platform/media/url_index.h" @@ -100,12 +102,22 @@ first_position_ = first_position; auto loader = std::make_unique<ResourceMultiBufferDataProvider>( - url_data_.get(), first_position_, false /* is_client_audio_element */, + url_data_.get(), first_position_, /*is_client_audio_element=*/false, task_environment_.GetMainThreadTaskRunner()); loader_ = loader.get(); url_data_->multibuffer()->AddProvider(std::move(loader)); } + std::unique_ptr<MultiBufferReader> CreateReader(int size) { + auto reader = std::make_unique<MultiBufferReader>( + url_data_->multibuffer(), 0, 1024 * 1024, + /*is_client_audio_element=*/false, base::DoNothing(), + task_environment_.GetMainThreadTaskRunner()); + reader->SetPinRange(0, 1024 * 1024); + reader->Wait(size, base::BindOnce([]() {})); + return reader; + } + void Start() { loader_->Start(); } void FullResponse(int64_t instance_size, bool ok = true) { @@ -197,9 +209,8 @@ std::unique_ptr<WebAssociatedURLLoader> CreateUrlLoader( const WebAssociatedURLLoaderOptions& options) { auto url_loader = std::make_unique<NiceMock<MockWebAssociatedURLLoader>>(); - EXPECT_CALL( - *url_loader.get(), - LoadAsynchronously(Truly(CorrectAcceptEncoding), loader_.get())); + EXPECT_CALL(*url_loader, LoadAsynchronously(Truly(CorrectAcceptEncoding), + loader_.get())); return url_loader; } @@ -358,4 +369,73 @@ StopWhenLoad(); } +// This test verifies that a cross-origin redirect sequence (A -> B -> A) +// does not bypass the multibuffer security checks. +TEST_F(ResourceMultiBufferDataProviderTest, NoCrossOriginMediaLeaks) { + // Switch to a `url_index_` with a real block size. + url_index_ = std::make_unique<UrlIndex>( + &fetch_context_, task_environment_.GetMainThreadTaskRunner()); + + constexpr char kOriginA1[] = "http://localhost:18080/v1"; + constexpr char kOriginB[] = "http://127.0.0.1:18081/secret"; + constexpr char kOriginA2[] = "http://localhost:18080/v2"; + constexpr int kBlockSize = 1 << 15; + + // 1. Setup + Initialize(kOriginA1, 0); + auto reader = CreateReader(kBlockSize); + task_environment_.FastForwardUntilNoTasksRemain(); + ASSERT_TRUE(loader_); + + loader_->Start(); + + // 2. Mark B as cross-origin (opaque response). + WebURL url_b{KURL(kOriginB)}; + WebURLResponse response_b{url_b}; + response_b.SetHttpStatusCode(206); + response_b.SetHttpHeaderField(WebString::FromUtf8("Content-Range"), + WebString::FromUtf8("bytes 0-1000000/1000001")); + response_b.SetExpectedContentLength(1000001); + response_b.SetType(network::mojom::FetchResponseType::kOpaque); + + ASSERT_TRUE(loader_->WillFollowRedirect(url_b, response_b)); + task_environment_.FastForwardUntilNoTasksRemain(); + + auto SetUrlDataAndCreateReader = + [&](const scoped_refptr<UrlData>& new_url_data) { + reader.reset(); + url_data_ = new_url_data; + reader = CreateReader(kBlockSize); + }; + + EXPECT_CALL(*this, RedirectCallback(_)) + .WillRepeatedly(SetUrlDataAndCreateReader); + loader_->DidReceiveResponse(response_b); + ASSERT_TRUE(loader_); + task_environment_.FastForwardUntilNoTasksRemain(); + ASSERT_TRUE(url_data_->is_cors_cross_origin()); + + // 3. Receive some data for B. This data is now in the MultiBuffer for B. + std::string data_str(kBlockSize, 'b'); + loader_->DidReceiveData(data_str); + + // 4. Simulate a failure that invokes a retry. + loader_->DidFail(WebURLError(net::ERR_ABORTED, url_b)); + task_environment_.FastForwardUntilNoTasksRemain(); + ASSERT_TRUE(loader_); + + WebURL url_a2{KURL(kOriginA2)}; + WebURLResponse response_a2{url_a2}; + response_a2.SetHttpStatusCode(206); + response_a2.SetHttpHeaderField( + WebString::FromUtf8("Content-Range"), + WebString::FromUtf8("bytes 32768-1000000/1000001")); + response_a2.SetExpectedContentLength(1000001 - kBlockSize); + response_a2.SetType(network::mojom::FetchResponseType::kBasic); + + // Redirecting back to A should fail cross origin checks. + EXPECT_FALSE(loader_->WillFollowRedirect(url_a2, response_a2)); + StopWhenLoad(); +} + } // namespace blink diff --git a/third_party/blink/renderer/platform/media/url_index.cc b/third_party/blink/renderer/platform/media/url_index.cc index 410b0ae8..2ec380e 100644 --- a/third_party/blink/renderer/platform/media/url_index.cc +++ b/third_party/blink/renderer/platform/media/url_index.cc @@ -148,6 +148,7 @@ DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); // Copy any cached data over to the new location. url_data->multibuffer()->MergeFrom(multibuffer()); + url_data->set_is_cors_cross_origin(is_cors_cross_origin()); Vector<RedirectCB> redirect_callbacks; redirect_callbacks.swap(redirect_callbacks_); diff --git a/third_party/blink/renderer/platform/media/url_index_unittest.cc b/third_party/blink/renderer/platform/media/url_index_unittest.cc index ebc07fea..a83c2d63b 100644 --- a/third_party/blink/renderer/platform/media/url_index_unittest.cc +++ b/third_party/blink/renderer/platform/media/url_index_unittest.cc @@ -208,4 +208,20 @@ EXPECT_FALSE(b->url_index()); } +TEST_F(UrlIndexTest, RedirectToPreservesCrossOrigin) { + KURL url1("http://foo.bar.com"); + KURL url2("http://bar.foo.com"); + scoped_refptr<UrlData> a = GetByUrl(url1, UrlData::CORS_UNSPECIFIED); + scoped_refptr<UrlData> b = GetByUrl(url2, UrlData::CORS_UNSPECIFIED); + + EXPECT_FALSE(a->is_cors_cross_origin()); + EXPECT_FALSE(b->is_cors_cross_origin()); + + a->set_is_cors_cross_origin(true); + a->RedirectTo(b); + + EXPECT_TRUE(a->is_cors_cross_origin()); + EXPECT_TRUE(b->is_cors_cross_origin()); +}
Regression Test / PoC
diff --git a/third_party/blink/renderer/platform/media/resource_multi_buffer_data_provider_unittest.cc b/third_party/blink/renderer/platform/media/resource_multi_buffer_data_provider_unittest.cc
index 0318e17..6431a01 100644
--- a/third_party/blink/renderer/platform/media/resource_multi_buffer_data_provider_unittest.cc
+++ b/third_party/blink/renderer/platform/media/resource_multi_buffer_data_provider_unittest.cc
@@ -14,6 +14,7 @@
#include "base/containers/heap_array.h"
#include "base/format_macros.h"
+#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/run_loop.h"
@@ -34,6 +35,7 @@
#include "third_party/blink/public/platform/web_url_error.h"
#include "third_party/blink/public/platform/web_url_request.h"
#include "third_party/blink/public/platform/web_url_response.h"
+#include "third_party/blink/renderer/platform/media/multi_buffer_reader.h"
#include "third_party/blink/renderer/platform/media/testing/mock_resource_fetch_context.h"
#include "third_party/blink/renderer/platform/media/testing/mock_web_associated_url_loader.h"
#include "third_party/blink/renderer/platform/media/url_index.h"
@@ -100,12 +102,22 @@
first_position_ = first_position;
auto loader = std::make_unique<ResourceMultiBufferDataProvider>(
- url_data_.get(), first_position_, false /* is_client_audio_element */,
+ url_data_.get(), first_position_, /*is_client_audio_element=*/false,
task_environment_.GetMainThreadTaskRunner());
loader_ = loader.get();
url_data_->multibuffer()->AddProvider(std::move(loader));
}
+ std::unique_ptr<MultiBufferReader> CreateReader(int size) {
+ auto reader = std::make_unique<MultiBufferReader>(
+ url_data_->multibuffer(), 0, 1024 * 1024,
+ /*is_client_audio_element=*/false, base::DoNothing(),
+ task_environment_.GetMainThreadTaskRunner());
+ reader->SetPinRange(0, 1024 * 1024);
+ reader->Wait(size, base::BindOnce([]() {}));
+ return reader;
+ }
+
void Start() { loader_->Start(); }
void FullResponse(int64_t instance_size, bool ok = true) {
@@ -197,9 +209,8 @@
std::unique_ptr<WebAssociatedURLLoader> CreateUrlLoader(
const WebAssociatedURLLoaderOptions& options) {
auto url_loader = std::make_unique<NiceMock<MockWebAssociatedURLLoader>>();
- EXPECT_CALL(
- *url_loader.get(),
- LoadAsynchronously(Truly(CorrectAcceptEncoding), loader_.get()));
+ EXPECT_CALL(*url_loader, LoadAsynchronously(Truly(CorrectAcceptEncoding),
+ loader_.get()));
return url_loader;
}
@@ -358,4 +369,73 @@
StopWhenLoad();
}
+// This test verifies that a cross-origin redirect sequence (A -> B -> A)
+// does not bypass the multibuffer security checks.
+TEST_F(ResourceMultiBufferDataProviderTest, NoCrossOriginMediaLeaks) {
+ // Switch to a `url_index_` with a real block size.
+ url_index_ = std::make_unique<UrlIndex>(
+ &fetch_context_, task_environment_.GetMainThreadTaskRunner());
+
+ constexpr char kOriginA1[] = "http://localhost:18080/v1";
+ constexpr char kOriginB[] = "http://127.0.0.1:18081/secret";
+ constexpr char kOriginA2[] = "http://localhost:18080/v2";
+ constexpr int kBlockSize = 1 << 15;
+
+ // 1. Setup
+ Initialize(kOriginA1, 0);
+ auto reader = CreateReader(kBlockSize);
+ task_environment_.FastForwardUntilNoTasksRemain();
+ ASSERT_TRUE(loader_);
+
+ loader_->Start();
+
+ // 2. Mark B as cross-origin (opaque response).
+ WebURL url_b{KURL(kOriginB)};
+ WebURLResponse response_b{url_b};
+ response_b.SetHttpStatusCode(206);
+ response_b.SetHttpHeaderField(WebString::FromUtf8("Content-Range"),
+ WebString::FromUtf8("bytes 0-1000000/1000001"));
+ response_b.SetExpectedContentLength(1000001);
+ response_b.SetType(network::mojom::FetchResponseType::kOpaque);
+
+ ASSERT_TRUE(loader_->WillFollowRedirect(url_b, response_b));
+ task_environment_.FastForwardUntilNoTasksRemain();
+
+ auto SetUrlDataAndCreateReader =
+ [&](const scoped_refptr<UrlData>& new_url_data) {
+ reader.reset();
+ url_data_ = new_url_data;
+ reader = CreateReader(kBlockSize);
+ };
+
+ EXPECT_CALL(*this, RedirectCallback(_))
+ .WillRepeatedly(SetUrlDataAndCreateReader);
+ loader_->DidReceiveResponse(response_b);
+ ASSERT_TRUE(loader_);
+ task_environment_.FastForwardUntilNoTasksRemain();
+ ASSERT_TRUE(url_data_->is_cors_cross_origin());
+
+ // 3. Receive some data for B. This data is now in the MultiBuffer for B.
+ std::string data_str(kBlockSize, 'b');
+ loader_->DidReceiveData(data_str);
+
+ // 4. Simulate a failure that invokes a retry.
+ loader_->DidFail(WebURLError(net::ERR_ABORTED, url_b));
+ task_environment_.FastForwardUntilNoTasksRemain();
+ ASSERT_TRUE(loader_);
+
+ WebURL url_a2{KURL(kOriginA2)};
+ WebURLResponse response_a2{url_a2};
+ response_a2.SetHttpStatusCode(206);
+ response_a2.SetHttpHeaderField(
+ WebString::FromUtf8("Content-Range"),
+ WebString::FromUtf8("bytes 32768-1000000/1000001"));
+ response_a2.SetExpectedContentLength(1000001 - kBlockSize);
+ response_a2.SetType(network::mojom::FetchResponseType::kBasic);
+
+ // Redirecting back to A should fail cross origin checks.
+ EXPECT_FALSE(loader_->WillFollowRedirect(url_a2, response_a2));
+ StopWhenLoad();
+}
+
} // namespace blink
diff --git a/third_party/blink/renderer/platform/media/url_index_unittest.cc b/third_party/blink/renderer/platform/media/url_index_unittest.cc
index ebc07fea..a83c2d63b 100644
--- a/third_party/blink/renderer/platform/media/url_index_unittest.cc
+++ b/third_party/blink/renderer/platform/media/url_index_unittest.cc
@@ -208,4 +208,20 @@
EXPECT_FALSE(b->url_index());
}
+TEST_F(UrlIndexTest, RedirectToPreservesCrossOrigin) {
+ KURL url1("http://foo.bar.com");
+ KURL url2("http://bar.foo.com");
+ scoped_refptr<UrlData> a = GetByUrl(url1, UrlData::CORS_UNSPECIFIED);
+ scoped_refptr<UrlData> b = GetByUrl(url2, UrlData::CORS_UNSPECIFIED);
+
+ EXPECT_FALSE(a->is_cors_cross_origin());
+ EXPECT_FALSE(b->is_cors_cross_origin());
+
+ a->set_is_cors_cross_origin(true);
+ a->RedirectTo(b);
+
+ EXPECT_TRUE(a->is_cors_cross_origin());
+ EXPECT_TRUE(b->is_cors_cross_origin());
+}
+
} // namespace blink
Original Bug Report
Cross-origin media leak via Service Worker redirect interception
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 without the Chrome Security team.
Overview: A logic flaw in ResourceMultiBufferDataProvider allows an attacker to bypass origin checks during media retries. By using a Service Worker to redirect a cross-origin retry back to the same origin, authenticated cross-origin data can be merged into a same-origin cache entry without the CORS taint flag. This potentially allows the attacker to read cross-origin media data via <canvas> or Web Audio.
Affected files:
third_party/blink/renderer/platform/media/resource_multi_buffer_data_provider.ccthird_party/blink/renderer/platform/media/url_index.cc
Estimated timestamp from git blame: 2025-08-11
Summary
A potential vulnerability in Blink’s media loader allows a cross-origin information leak. An attacker can use a Service Worker to intercept a retried media request and redirect it in a way that causes authenticated, cross-origin data to be merged into a same-origin buffer. This data can then be read by the attacker using standard web APIs like <canvas> or Web Audio, bypassing the Same-Origin Policy (SOP).
Vulnerability Details
The vulnerability stems from how ResourceMultiBufferDataProvider handles origin checks during redirects and how UrlData merges data:
- Stale Origin Check: In
ResourceMultiBufferDataProvider::WillFollowRedirect, the security checkSecurityOrigin::AreSameOrigin(original_url_, redirects_to_)compares the redirect destination againstoriginal_url_. However,original_url_is initialized once in the constructor. If a request is redirected cross-origin, and a subsequent retry is redirected back to the original origin by a Service Worker, this check evaluates to true. - Bypassed Empty Buffer Check: Because the
AreSameOrigincheck passes, the vital security checkif (url_data_->multibuffer()->map().empty() && fifo_.empty())is skipped. This allows the redirect to proceed even if the buffers are already full of cross-origin data from the previous target. - Missing Taint Propagation: The loader calls
UrlData::RedirectToto migrate to the new same-originUrlData. This method callsMultiBuffer::MergeFrom, which copies the cross-origin data blocks but fails to propagate theis_cors_cross_origin_taint flag. The cross-origin data is now cached as same-origin.
Potential Attack Scenario
An attacker could potentially exploit this via the following steps (note: this is a theoretical sequence, as our tooling cannot currently execute a PoC):
- An attacker’s page on Origin A (controlled by a Service Worker) starts a media request to
https://A/v1(CORS unspecified). - The server redirects to a victim resource
https://B/secret(authenticated via cookies). - The loader follows the redirect, fetching and buffering cross-origin bytes from
https://B/secretinto a cross-originUrlDataobject. - The connection is interrupted (e.g., via the attacker serving a capped 206 response), triggering the loader to schedule a retry to
https://B/secret. - The attacker’s Service Worker intercepts this cross-origin retry and responds with a redirect back to
https://A/v2. WillFollowRedirectcompareshttps://A/v1(original_url_) andhttps://A/v2. They are same-origin, so the empty-buffer check is bypassed.- The Service Worker responds to the
https://A/v2fetch with a basic same-origin response. - The loader calls
UrlData::RedirectTo, merging the cross-origin bytes fromhttps://B/secretinto the same-originUrlDataforhttps://A/v2without setting theis_cors_cross_origin_flag. - The attacker creates a new
<video src="https://A/v2">element. It hits the per-frameUrlIndexcache, returning the poisonedUrlData. - Because the new element hasn’t been redirected and the
UrlDatais not flagged as cross-origin, the video is not tainted. The attacker reads the victim’s bytes using<canvas>or Web Audio.
Suggested Fix
There are two primary areas to address:
- Fix the Origin Check in
WillFollowRedirect: The comparison should likely validate against the current URL (url_data_->url()) rather than the immutableoriginal_url_, or track if a cross-origin redirect has already occurred to enforce the empty buffer check more strictly. - Ensure Safe Merging in
UrlData::RedirectTo: When merging multibuffers during a redirect, theis_cors_cross_origin_flag (and other relevant security metadata) should be safely transferred or validated to ensure cross-origin data cannot be laundered into a same-origin cache entry.
Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b
Results 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.