CVE-2026-76027
Overview
Background
- CacheStorage
- The Service Worker Cache API backing store (
content/browser/cache_storage/).cache.put(request, response)persists aFetchAPIResponseinto a disk_cache entry, serialized viaproto::CacheResponse;cache.match()/CreateResponsereconstructs aFetchAPIResponsefrom that proto. - `has_range_requested`
- A bit on
FetchAPIResponserecording that the response was produced for a range request.ResourceLoader::DidReceiveResponseInternaluses 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_requestedis 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.
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
- Register a Service Worker Attacker page controls a SW on its origin (standard capability).
- 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). - Store it in CacheStorage
cache.put(key, opaqueRangeResponse)persists the response;has_range_requestedis dropped to false in the proto. - Replay to a non-range context
Later the SW answers an ordinary
fetch(..., {mode:'no-cors'})/<img>/<script>load withcache.match(key); pre-patch the loader accepts it becausehas_range_requestedis false. - 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
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Pcontent/browser/cache_storage/cache_storage_cache_unittest.cc |
modified |
Files Changed
content/browser/cache_storage/cache_storage_cache.cccontent/browser/cache_storage/cache_storage_cache_unittest.ccthird_party/blink/web_tests/external/wpt/fetch/range/sw.https.window.jsthird_party/blink/web_tests/external/wpt/fetch/range/resources/range-sw.jscontent/browser/cache_storage/cache_storage.protothird_party/blink/web_tests/external/wpt/fetch/range/sw-416.https.window.js
Audit Directions
- Dropped-on-persist flagsAudit
PutDidCreateEntry/CreateResponse(and other proto (de)serializers in CacheStorage, Background Fetch, HTTP cache) for security-relevantFetchAPIResponsebits that are set in one direction but not the other. - Request-vs-response header confusionSearch 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 reachabilityReview every path feeding
ResourceLoader::DidReceiveResponseInternal’s opaque-range check (SW-synthesized responses, Background Fetch, navigation preload) to ensurehas_range_requestedis trustworthy end to end.
Patch
From edd9a6c0d0c168f442a480731e3abc22f0fb96c2 Mon Sep 17 00:00:00 2001
From: Eriko Kurimoto <elkurin@chromium.org>
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 <elkurin@chromium.org>
Reviewed-by: Mingyu Lei <leimy@chromium.org>
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
Regression Test / PoC
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