Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient data validation in Media
DescriptionInsufficient data validation in Media
ComponentMedia
Bug ClassLogic Error
Tracker501640084
Fix commit5a92a8c2fe28 (chromium/src) +103/-5
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/platform/media/resource_multi_buffer_data_provider.cc
modified
TEST_F
third_party/blink/renderer/platform/media/resource_multi_buffer_data_provider_unittest.cc
modified
TEST_F
third_party/blink/renderer/platform/media/url_index_unittest.cc
modified

Files Changed

  • third_party/blink/renderer/platform/media/resource_multi_buffer_data_provider.cc
  • third_party/blink/renderer/platform/media/resource_multi_buffer_data_provider_unittest.cc
  • third_party/blink/renderer/platform/media/url_index.cc
  • third_party/blink/renderer/platform/media/url_index_unittest.cc
From 5a92a8c2fe282cb330e6ca8a05f06b24aa310bfe Mon Sep 17 00:00:00 2001
From: Dale Curtis <dalecurtis@chromium.org>
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 <tmathmeyer@chromium.org>
Commit-Queue: Ted (Chromium) Meyer <tmathmeyer@chromium.org>
Commit-Queue: Dale Curtis <dalecurtis@chromium.org>
Auto-Submit: Dale Curtis <dalecurtis@chromium.org>
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());
+}
Loading diff…

Regression Test / PoC

shipped with the fix
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
Loading diff…

Original Bug Report

reported by vm...@google.com

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.cc
  • third_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:

  1. Stale Origin Check: In ResourceMultiBufferDataProvider::WillFollowRedirect, the security check SecurityOrigin::AreSameOrigin(original_url_, redirects_to_) compares the redirect destination against original_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.
  2. Bypassed Empty Buffer Check: Because the AreSameOrigin check passes, the vital security check if (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.
  3. Missing Taint Propagation: The loader calls UrlData::RedirectTo to migrate to the new same-origin UrlData. This method calls MultiBuffer::MergeFrom, which copies the cross-origin data blocks but fails to propagate the is_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):

  1. An attacker’s page on Origin A (controlled by a Service Worker) starts a media request to https://A/v1 (CORS unspecified).
  2. The server redirects to a victim resource https://B/secret (authenticated via cookies).
  3. The loader follows the redirect, fetching and buffering cross-origin bytes from https://B/secret into a cross-origin UrlData object.
  4. 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.
  5. The attacker’s Service Worker intercepts this cross-origin retry and responds with a redirect back to https://A/v2.
  6. WillFollowRedirect compares https://A/v1 (original_url_) and https://A/v2. They are same-origin, so the empty-buffer check is bypassed.
  7. The Service Worker responds to the https://A/v2 fetch with a basic same-origin response.
  8. The loader calls UrlData::RedirectTo, merging the cross-origin bytes from https://B/secret into the same-origin UrlData for https://A/v2 without setting the is_cors_cross_origin_ flag.
  9. The attacker creates a new <video src="https://A/v2"> element. It hits the per-frame UrlIndex cache, returning the poisoned UrlData.
  10. Because the new element hasn’t been redirected and the UrlData is 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:

  1. Fix the Origin Check in WillFollowRedirect: The comparison should likely validate against the current URL (url_data_->url()) rather than the immutable original_url_, or track if a cross-origin redirect has already occurred to enforce the empty buffer check more strictly.
  2. Ensure Safe Merging in UrlData::RedirectTo: When merging multibuffers during a redirect, the is_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.

View on issue tracker