High chrome Cross Origin 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMemory corruption in CacheStorage
DescriptionMemory corruption in CacheStorage
ComponentCacheStorage
Sub-componentCacheStorage
Bug ClassCross Origin
Tracker568894475
Fix commitedd9a6c0d0c1 (chromium/src) +144/-3
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-29

Background

CacheStorage
The Service Worker Cache API backing store (content/browser/cache_storage/). cache.put(request, response) persists a FetchAPIResponse into a disk_cache entry, serialized via proto::CacheResponse; cache.match()/CreateResponse reconstructs a FetchAPIResponse from that proto.
`has_range_requested`
A bit on FetchAPIResponse recording that the response was produced for a range request. ResourceLoader::DidReceiveResponseInternal uses it to decide whether to run the security check that rejects opaque partial (206) / 416 responses when the current request is not a range request.
Opaque 206 / 416
Cross-origin no-cors range responses. The Fetch spec forbids replaying an opaque range response to a non-range context, because doing so can leak cross-origin byte ranges / be used as a confused-deputy against the content.
`proto::CacheResponse`
The persisted schema; new field 19 has_range_requested is added by this patch.
`CacheStorageCache::CreateResponse` / `PutDidCreateEntry`
Deserialize and serialize paths respectively; the two functions where the bit was mishandled.

Root Cause Analysis

When persisting a cached response, PutDidCreateEntry serialized many response fields (request_include_credentials, timing_allow_passed, …) but never wrote has_range_requested into proto::CacheResponse. On read-back, CreateResponse tried to recompute the bit with headers.contains(net::HttpRequestHeaders::kRange) — testing for a Range request header on the stored response headers, which is never present. The two mistakes compound: the true value was dropped on write, and the recompute on read always produced false. Consequently every response served from CacheStorage came back with has_range_requested == false.

That reset defeats the downstream guard in ResourceLoader::DidReceiveResponseInternal: with has_range_requested false, the loader skips the check that rejects opaque 206/416 responses for non-range requests. An attacker could store an opaque cross-origin 206 Partial Content (or 416) response in CacheStorage via a Service Worker and later have the SW return it for an ordinary non-range request (image/script/fetch), bypassing the spec-mandated rejection and surfacing cross-origin partial content where it should be blocked. The patch persists the real bit (response_metadata->set_has_range_requested(...)) and, for backward compatibility with entries written before field 19 existed, makes CreateResponse force has_range_requested = true for any opaque response whose status is HTTP_PARTIAL_CONTENT or HTTP_REQUESTED_RANGE_NOT_SATISFIABLE, so such responses can never be replayed to a non-range request. New WPTs (sw.https.window.js, sw-416.https.window.js) confirm a cached opaque ranged response now rejects.

The advisory calls this “Memory corruption in CacheStorage,” but the diff shows no memory-safety sink — it is a dropped/incorrectly-recomputed security flag that bypasses a cross-origin range-response restriction. The substantiated class is a cross-origin policy bypass / information-disclosure, not memory corruption.

Key insight
A security-relevant flag (has_range_requested) was never persisted and was “recomputed” by looking for a request-only header on a response, so it always read back false — silently disabling the check that stops opaque cross-origin range responses from being replayed to non-range requests.

Attack Path

  1. Register a Service Worker Attacker page controls a SW on its origin (standard capability).
  2. Fetch a cross-origin opaque range response The SW issues a no-cors range request to a victim/remote host that returns an opaque 206 (or a 416), e.g. via media/audio element range fetch (the added WPT does exactly this with partial-script.py).
  3. Store it in CacheStorage cache.put(key, opaqueRangeResponse) persists the response; has_range_requested is dropped to false in the proto.
  4. Replay to a non-range context Later the SW answers an ordinary fetch(..., {mode:'no-cors'}) / <img>/<script> load with cache.match(key); pre-patch the loader accepts it because has_range_requested is false.
  5. Bypass achieved A partial/opaque cross-origin response is delivered to a non-range request, defeating the Fetch opaque-range rejection (potential cross-origin content/side-channel exposure).

Impact Assessment

This is a renderer-reachable logic flaw enforced in the browser process (CacheStorage) plus the renderer loader. No special privilege is needed beyond running script and a Service Worker on an attacker origin. The gained primitive is a bypass of the opaque-range-response security check — a cross-origin confidentiality/policy violation, not memory corruption and not RCE. Preconditions: ability to obtain and cache an opaque 206/416 from a target, then replay it to a non-range request. Impact strength is a cross-origin read/side-channel against partial content, bounded by what opaque responses already reveal.

Changed Functions

FunctionChangeNotes
TEST_P
content/browser/cache_storage/cache_storage_cache_unittest.cc
modified

Files Changed

  • content/browser/cache_storage/cache_storage_cache.cc
  • content/browser/cache_storage/cache_storage_cache_unittest.cc
  • third_party/blink/web_tests/external/wpt/fetch/range/sw.https.window.js
  • third_party/blink/web_tests/external/wpt/fetch/range/resources/range-sw.js
  • content/browser/cache_storage/cache_storage.proto
  • third_party/blink/web_tests/external/wpt/fetch/range/sw-416.https.window.js

Audit Directions

  • Dropped-on-persist flags
    Audit PutDidCreateEntry / CreateResponse (and other proto (de)serializers in CacheStorage, Background Fetch, HTTP cache) for security-relevant FetchAPIResponse bits that are set in one direction but not the other.
  • Request-vs-response header confusion
    Search for checks that inspect response headers for request-only headers (Range, Authorization) or vice versa, which silently evaluate to a fixed value.
  • Opaque-range enforcement reachability
    Review every path feeding ResourceLoader::DidReceiveResponseInternal’s opaque-range check (SW-synthesized responses, Background Fetch, navigation preload) to ensure has_range_requested is trustworthy end to end.
From edd9a6c0d0c168f442a480731e3abc22f0fb96c2 Mon Sep 17 00:00:00 2001
From: Eriko Kurimoto <[email protected]>
Date: Thu, 22 Oct 2026 07:46:24 -0700
Subject: [PATCH] [CacheStorage] Persist has_range_requested on cached responses

Previously, CacheStorageCache::PutDidCreateEntry did not persist
FetchAPIResponse::has_range_requested in proto::CacheResponse, and
CreateResponse checked whether the response headers contained a Range
header (which is a request header and never present on responses). As a
result, cached responses returned by CacheStorage always had
has_range_requested reset to false.

When an opaque 206 Partial Content or 416 Range Not Satisfiable
response was stored in CacheStorage and later returned by a Service
Worker, ResourceLoader::DidReceiveResponseInternal skipped the check
that rejects opaque range responses for non-range requests.

Persist has_range_requested in proto::CacheResponse, and ensure
CreateResponse also treats opaque 206 and 416 responses as
range-requested even if the stored proto field is unset or false.

Bug: 568894475
Change-Id: I9c4d1f2a3e8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8618165
Commit-Queue: Eriko Kurimoto <[email protected]>
Reviewed-by: Mingyu Lei <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1724757}
---
diff --git a/content/browser/cache_storage/cache_storage.proto b/content/browser/cache_storage/cache_storage.proto
index d449f0d..916f53f7 100644
--- a/content/browser/cache_storage/cache_storage.proto
+++ b/content/browser/cache_storage/cache_storage.proto
@@ -86,6 +86,7 @@
   // than consuming this field directly.
   optional bool request_include_credentials = 17;
   optional bool timing_allow_passed = 18;
+  optional bool has_range_requested = 19;
 }
 
 message CacheMetadata {
diff --git a/content/browser/cache_storage/cache_storage_cache.cc b/content/browser/cache_storage/cache_storage_cache.cc
index a21c4d8..12d3c52 100644
--- a/content/browser/cache_storage/cache_storage_cache.cc
+++ b/content/browser/cache_storage/cache_storage_cache.cc
@@ -545,9 +545,16 @@
                                  ? metadata.response().timing_allow_passed()
                                  : true;
 
-  // While we block most partial responses from being stored, we can have
-  // partial responses for bgfetch or opaque responses.
-  bool has_range_requested = headers.contains(net::HttpRequestHeaders::kRange);
+  // The stored `has_range_requested` bit is supplied by the renderer via
+  // `cache.put()`, and entries written before the proto field was added do not
+  // have it set. Treat opaque 206 and 416 responses as range-requested so
+  // they cannot be replayed to non-range requests.
+  bool has_range_requested =
+      metadata.response().has_range_requested() ||
+      (response_type == network::mojom::FetchResponseType::kOpaque &&
+       (metadata.response().status_code() == net::HTTP_PARTIAL_CONTENT ||
+        metadata.response().status_code() ==
+            net::HTTP_REQUESTED_RANGE_NOT_SATISFIABLE));
 
   return blink::mojom::FetchAPIResponse::New(
       url_list, metadata.response().status_code(),
@@ -2043,6 +2050,8 @@
       put_context->response->request_include_credentials);
   response_metadata->set_timing_allow_passed(
       put_context->response->timing_allow_passed);
+  response_metadata->set_has_range_requested(
+      put_context->response->has_range_requested);
 
   // Get a temporary copy of the entry pointer before passing it in base::Bind.
   disk_cache::Entry* temp_entry_ptr = put_context->cache_entry.get();
diff --git a/content/browser/cache_storage/cache_storage_cache_unittest.cc b/content/browser/cache_storage/cache_storage_cache_unittest.cc
index 01d0356a..5ce683c 100644
--- a/content/browser/cache_storage/cache_storage_cache_unittest.cc
+++ b/content/browser/cache_storage/cache_storage_cache_unittest.cc
@@ -64,6 +64,7 @@
 #include "net/base/url_util.h"
 #include "net/disk_cache/disk_cache.h"
 #include "net/http/http_connection_info.h"
+#include "net/http/http_status_code.h"
 #include "storage/browser/blob/blob_storage_context.h"
 #include "storage/browser/quota/quota_manager_proxy.h"
 #include "storage/browser/test/blob_test_utils.h"
@@ -3378,6 +3379,46 @@
   EXPECT_TRUE(callback_response_->timing_allow_passed);
 }
 
+TEST_P(CacheStorageCacheTestP, HasRangeRequested) {
+  EXPECT_TRUE(Keys());
+
+  // Put a response with `has_range_requested = false`.
+  blink::mojom::FetchAPIResponsePtr response = CreateNoBodyResponse();
+  response->has_range_requested = false;
+  EXPECT_TRUE(Put(no_body_request_, std::move(response)));
+
+  // Match the response and verify `has_range_requested` is preserved as
+  // `false`.
+  EXPECT_TRUE(Match(no_body_request_));
+  ASSERT_TRUE(callback_response_);
+  EXPECT_FALSE(callback_response_->has_range_requested);
+
+  // Put a response with `has_range_requested = true`.
+  blink::mojom::FetchAPIResponsePtr response2 = CreateBlobBodyResponse();
+  response2->has_range_requested = true;
+  EXPECT_TRUE(Put(body_request_, std::move(response2)));
+
+  // Match the response and verify `has_range_requested` is preserved as `true`.
+  EXPECT_TRUE(Match(body_request_));
+  ASSERT_TRUE(callback_response_);
+  EXPECT_TRUE(callback_response_->has_range_requested);
+
+  // Opaque 206 and 416 responses force `has_range_requested = true` even if
+  // stored with `has_range_requested = false`.
+  for (int status_code :
+       {net::HTTP_PARTIAL_CONTENT, net::HTTP_REQUESTED_RANGE_NOT_SATISFIABLE}) {
+    blink::mojom::FetchAPIResponsePtr opaque_partial = CreateBlobBodyResponse();
+    opaque_partial->response_type = network::mojom::FetchResponseType::kOpaque;
+    opaque_partial->status_code = status_code;
+    opaque_partial->has_range_requested = false;
+    EXPECT_TRUE(Put(body_request_, std::move(opaque_partial)));
+
+    EXPECT_TRUE(Match(body_request_));
+    ASSERT_TRUE(callback_response_);
+    EXPECT_TRUE(callback_response_->has_range_requested);
+  }
+}
+
 INSTANTIATE_TEST_SUITE_P(CacheStorageCacheTest,
                          CacheStorageCacheTestP,
                          ::testing::Values(false, true));
diff --git a/third_party/blink/web_tests/external/wpt/fetch/range/resources/range-sw.js b/third_party/blink/web_tests/external/wpt/fetch/range/resources/range-sw.js
index dfa537a2..2f25f72 100644
--- a/third_party/blink/web_tests/external/wpt/fetch/range/resources/range-sw.js
+++ b/third_party/blink/web_tests/external/wpt/fetch/range/resources/range-sw.js
@@ -31,6 +31,12 @@
     case 'use-stored-ranged-response':
       useStoredRangeResponse(event);
       return;
+    case 'cache-ranged-response':
+      cacheRangedResponse(event);
+      return;
+    case 'use-cached-ranged-response':
+      useCachedRangeResponse(event);
+      return;
     case 'broadcast-accept-encoding':
       broadcastAcceptEncoding(event);
       return;
@@ -148,6 +154,32 @@
   }());
 }
 
+const RANGE_CACHE_NAME = 'range-response-cache';
+const RANGE_CACHE_KEY = 'cached-ranged-response';
+
+function cacheRangedResponse(event) {
+  /** @type Request */
+  const request = event.request;
+  const id = new URL(request.url).searchParams.get('id');
+
+  event.waitUntil((async () => {
+    const response = await fetch(event.request);
+    const cache = await caches.open(RANGE_CACHE_NAME);
+    await cache.put(RANGE_CACHE_KEY, response);
+    broadcast({ id });
+  })());
+
+  // Just send back any response, it isn't important for the test.
+  event.respondWith(new Response(''));
+}
+
+function useCachedRangeResponse(event) {
+  event.respondWith((async () => {
+    const cache = await caches.open(RANGE_CACHE_NAME);
+    return cache.match(RANGE_CACHE_KEY);
+  })());
+}
+
 function broadcastAcceptEncoding(event) {
   /** @type Request */
   const request = event.request;
diff --git a/third_party/blink/web_tests/external/wpt/fetch/range/sw-416.https.window.js b/third_party/blink/web_tests/external/wpt/fetch/range/sw-416.https.window.js
index ca4436a..30d20628 100644
--- a/third_party/blink/web_tests/external/wpt/fetch/range/sw-416.https.window.js
+++ b/third_party/blink/web_tests/external/wpt/fetch/range/sw-416.https.window.js
@@ -59,3 +59,30 @@
   const loadImagePromise = loadImage('?action=use-stored-ranged-response', { doc: w.document });
   await promise_rejects_js(t, Error, loadImagePromise);
 }, `416 response not allowed following no-cors ranged request`);
+
+promise_test(async t => {
+  const scope = BASE_SCOPE + Math.random();
+  await setupRegistration(t, scope);
+  const iframe = await with_iframe(scope);
+  const w = iframe.contentWindow;
+  const id = Math.random() + '';
+  const cachedRangeResponse = awaitMessage(w.navigator.serviceWorker, id);
+
+  const url = new URL('partial-script.py', w.location);
+  url.searchParams.set('require-range', '1');
+  url.searchParams.set('range-not-satisfiable', '1');
+  url.searchParams.set('type', 'image/png');
+  url.searchParams.set('action', 'cache-ranged-response');
+  url.searchParams.set('id', id);
+  url.hostname = REMOTE_HOST;
+
+  appendAudio(w.document, url);
+
+  await cachedRangeResponse;
+
+  const fetchPromise = w.fetch('?action=use-cached-ranged-response', { mode: 'no-cors' });
+  await promise_rejects_js(t, w.TypeError, fetchPromise);
+
+  const loadImagePromise = loadImage('?action=use-cached-ranged-response', { doc: w.document });
+  await promise_rejects_js(t, Error, loadImagePromise);
+}, `Cached 416 response not allowed following no-cors ranged request`);
diff --git a/third_party/blink/web_tests/external/wpt/fetch/range/sw.https.window.js b/third_party/blink/web_tests/external/wpt/fetch/range/sw.https.window.js
index 8d410c3..5234150 100644
--- a/third_party/blink/web_tests/external/wpt/fetch/range/sw.https.window.js
+++ b/third_party/blink/web_tests/external/wpt/fetch/range/sw.https.window.js
@@ -103,6 +103,37 @@
   const iframe = await with_iframe(scope);
   const w = iframe.contentWindow;
   const id = Math.random() + '';
+  const cachedRangeResponse = awaitMessage(t, w.navigator.serviceWorker, id);
+
+  // Trigger a cross-origin range request using media and store the opaque 206
+  // response in Cache Storage.
+  const url = new URL('partial-script.py', w.location);
+  url.searchParams.set('require-range', '1');
+  url.searchParams.set('action', 'cache-ranged-response');
+  url.searchParams.set('id', id);
+  url.hostname = REMOTE_HOST;
+
+  appendAudio(w.document, url);
+
+  await cachedRangeResponse;
+
+  // Fetching should reject
+  const fetchPromise = w.fetch('?action=use-cached-ranged-response', { mode: 'no-cors' });
+  await promise_rejects_js(t, w.TypeError, fetchPromise);
+
+  // Script loading should error too
+  const loadScriptPromise = loadScript('?action=use-cached-ranged-response', { doc: w.document });
+  await promise_rejects_js(t, Error, loadScriptPromise);
+
+  assert_false(!!w.scriptExecuted, `Cached partial response shouldn't be executed`);
+}, `Cached ranged response not allowed following no-cors ranged request`);
+
+promise_test(async t => {
+  const scope = BASE_SCOPE + Math.random();
+  await setupRegistration(t, scope);
+  const iframe = await with_iframe(scope);
+  const w = iframe.contentWindow;
+  const id = Math.random() + '';
   const storedRangeResponse = awaitMessage(t, w.navigator.serviceWorker, id);
 
   // Trigger a range request using media
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/cache_storage/cache_storage_cache_unittest.cc b/content/browser/cache_storage/cache_storage_cache_unittest.cc
index 01d0356a..5ce683c 100644
--- a/content/browser/cache_storage/cache_storage_cache_unittest.cc
+++ b/content/browser/cache_storage/cache_storage_cache_unittest.cc
@@ -64,6 +64,7 @@
 #include "net/base/url_util.h"
 #include "net/disk_cache/disk_cache.h"
 #include "net/http/http_connection_info.h"
+#include "net/http/http_status_code.h"
 #include "storage/browser/blob/blob_storage_context.h"
 #include "storage/browser/quota/quota_manager_proxy.h"
 #include "storage/browser/test/blob_test_utils.h"
@@ -3378,6 +3379,46 @@
   EXPECT_TRUE(callback_response_->timing_allow_passed);
 }
 
+TEST_P(CacheStorageCacheTestP, HasRangeRequested) {
+  EXPECT_TRUE(Keys());
+
+  // Put a response with `has_range_requested = false`.
+  blink::mojom::FetchAPIResponsePtr response = CreateNoBodyResponse();
+  response->has_range_requested = false;
+  EXPECT_TRUE(Put(no_body_request_, std::move(response)));
+
+  // Match the response and verify `has_range_requested` is preserved as
+  // `false`.
+  EXPECT_TRUE(Match(no_body_request_));
+  ASSERT_TRUE(callback_response_);
+  EXPECT_FALSE(callback_response_->has_range_requested);
+
+  // Put a response with `has_range_requested = true`.
+  blink::mojom::FetchAPIResponsePtr response2 = CreateBlobBodyResponse();
+  response2->has_range_requested = true;
+  EXPECT_TRUE(Put(body_request_, std::move(response2)));
+
+  // Match the response and verify `has_range_requested` is preserved as `true`.
+  EXPECT_TRUE(Match(body_request_));
+  ASSERT_TRUE(callback_response_);
+  EXPECT_TRUE(callback_response_->has_range_requested);
+
+  // Opaque 206 and 416 responses force `has_range_requested = true` even if
+  // stored with `has_range_requested = false`.
+  for (int status_code :
+       {net::HTTP_PARTIAL_CONTENT, net::HTTP_REQUESTED_RANGE_NOT_SATISFIABLE}) {
+    blink::mojom::FetchAPIResponsePtr opaque_partial = CreateBlobBodyResponse();
+    opaque_partial->response_type = network::mojom::FetchResponseType::kOpaque;
+    opaque_partial->status_code = status_code;
+    opaque_partial->has_range_requested = false;
+    EXPECT_TRUE(Put(body_request_, std::move(opaque_partial)));
+
+    EXPECT_TRUE(Match(body_request_));
+    ASSERT_TRUE(callback_response_);
+    EXPECT_TRUE(callback_response_->has_range_requested);
+  }
+}
+
 INSTANTIATE_TEST_SUITE_P(CacheStorageCacheTest,
                          CacheStorageCacheTestP,
                          ::testing::Values(false, true));
diff --git a/third_party/blink/web_tests/external/wpt/fetch/range/resources/range-sw.js b/third_party/blink/web_tests/external/wpt/fetch/range/resources/range-sw.js
index dfa537a2..2f25f72 100644
--- a/third_party/blink/web_tests/external/wpt/fetch/range/resources/range-sw.js
+++ b/third_party/blink/web_tests/external/wpt/fetch/range/resources/range-sw.js
@@ -31,6 +31,12 @@
     case 'use-stored-ranged-response':
       useStoredRangeResponse(event);
       return;
+    case 'cache-ranged-response':
+      cacheRangedResponse(event);
+      return;
+    case 'use-cached-ranged-response':
+      useCachedRangeResponse(event);
+      return;
     case 'broadcast-accept-encoding':
       broadcastAcceptEncoding(event);
       return;
@@ -148,6 +154,32 @@
   }());
 }
 
+const RANGE_CACHE_NAME = 'range-response-cache';
+const RANGE_CACHE_KEY = 'cached-ranged-response';
+
+function cacheRangedResponse(event) {
+  /** @type Request */
+  const request = event.request;
+  const id = new URL(request.url).searchParams.get('id');
+
+  event.waitUntil((async () => {
+    const response = await fetch(event.request);
+    const cache = await caches.open(RANGE_CACHE_NAME);
+    await cache.put(RANGE_CACHE_KEY, response);
+    broadcast({ id });
+  })());
+
+  // Just send back any response, it isn't important for the test.
+  event.respondWith(new Response(''));
+}
+
+function useCachedRangeResponse(event) {
+  event.respondWith((async () => {
+    const cache = await caches.open(RANGE_CACHE_NAME);
+    return cache.match(RANGE_CACHE_KEY);
+  })());
+}
+
 function broadcastAcceptEncoding(event) {
   /** @type Request */
   const request = event.request;
diff --git a/third_party/blink/web_tests/external/wpt/fetch/range/sw-416.https.window.js b/third_party/blink/web_tests/external/wpt/fetch/range/sw-416.https.window.js
index ca4436a..30d20628 100644
--- a/third_party/blink/web_tests/external/wpt/fetch/range/sw-416.https.window.js
+++ b/third_party/blink/web_tests/external/wpt/fetch/range/sw-416.https.window.js
@@ -59,3 +59,30 @@
   const loadImagePromise = loadImage('?action=use-stored-ranged-response', { doc: w.document });
   await promise_rejects_js(t, Error, loadImagePromise);
 }, `416 response not allowed following no-cors ranged request`);
+
+promise_test(async t => {
+  const scope = BASE_SCOPE + Math.random();
+  await setupRegistration(t, scope);
+  const iframe = await with_iframe(scope);
+  const w = iframe.contentWindow;
+  const id = Math.random() + '';
+  const cachedRangeResponse = awaitMessage(w.navigator.serviceWorker, id);
+
+  const url = new URL('partial-script.py', w.location);
+  url.searchParams.set('require-range', '1');
+  url.searchParams.set('range-not-satisfiable', '1');
+  url.searchParams.set('type', 'image/png');
+  url.searchParams.set('action', 'cache-ranged-response');
+  url.searchParams.set('id', id);
+  url.hostname = REMOTE_HOST;
+
+  appendAudio(w.document, url);
+
+  await cachedRangeResponse;
+
+  const fetchPromise = w.fetch('?action=use-cached-ranged-response', { mode: 'no-cors' });
+  await promise_rejects_js(t, w.TypeError, fetchPromise);
+
+  const loadImagePromise = loadImage('?action=use-cached-ranged-response', { doc: w.document });
+  await promise_rejects_js(t, Error, loadImagePromise);
+}, `Cached 416 response not allowed following no-cors ranged request`);
diff --git a/third_party/blink/web_tests/external/wpt/fetch/range/sw.https.window.js b/third_party/blink/web_tests/external/wpt/fetch/range/sw.https.window.js
index 8d410c3..5234150 100644
--- a/third_party/blink/web_tests/external/wpt/fetch/range/sw.https.window.js
+++ b/third_party/blink/web_tests/external/wpt/fetch/range/sw.https.window.js
@@ -103,6 +103,37 @@
   const iframe = await with_iframe(scope);
   const w = iframe.contentWindow;
   const id = Math.random() + '';
+  const cachedRangeResponse = awaitMessage(t, w.navigator.serviceWorker, id);
+
+  // Trigger a cross-origin range request using media and store the opaque 206
+  // response in Cache Storage.
+  const url = new URL('partial-script.py', w.location);
+  url.searchParams.set('require-range', '1');
+  url.searchParams.set('action', 'cache-ranged-response');
+  url.searchParams.set('id', id);
+  url.hostname = REMOTE_HOST;
+
+  appendAudio(w.document, url);
+
+  await cachedRangeResponse;
+
+  // Fetching should reject
+  const fetchPromise = w.fetch('?action=use-cached-ranged-response', { mode: 'no-cors' });
+  await promise_rejects_js(t, w.TypeError, fetchPromise);
+
+  // Script loading should error too
+  const loadScriptPromise = loadScript('?action=use-cached-ranged-response', { doc: w.document });
+  await promise_rejects_js(t, Error, loadScriptPromise);
+
+  assert_false(!!w.scriptExecuted, `Cached partial response shouldn't be executed`);
+}, `Cached ranged response not allowed following no-cors ranged request`);
+
+promise_test(async t => {
+  const scope = BASE_SCOPE + Math.random();
+  await setupRegistration(t, scope);
+  const iframe = await with_iframe(scope);
+  const w = iframe.contentWindow;
+  const id = Math.random() + '';
   const storedRangeResponse = awaitMessage(t, w.navigator.serviceWorker, id);
 
   // Trigger a range request using media
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.