0

[Blob URL] Extend cross-partition blob URL fetching bypass to dedicated workers

If a document context requests storage access, has it granted, and then
creates a dedicated worker, that dedicated worker should be considered
to have storage access as well. This means that if the third-party
context creates a blob URL using the StorageAccessHandle and sends it to
the dedicated worker, the dedicated worker should be able to fetch it.

Bug: 403297818
Change-Id: Ifebc1f5ec78bd0e8e79e48ed21f6c98b90def715
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6409631
Reviewed-by: Bo Liu <boliu@chromium.org>
Reviewed-by: Fergal Daly <fergal@chromium.org>
Reviewed-by: Andrew Williams <awillia@chromium.org>
Reviewed-by: Robert Flack <flackr@chromium.org>
Commit-Queue: Janice Liu <janiceliu@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1443531}
This commit is contained in:
Janice Liu
2025-04-07 09:22:22 -07:00
committed by Chromium LUCI CQ
parent 882174e6f9
commit 557c11c341
14 changed files with 146 additions and 14 deletions

@ -146,7 +146,8 @@ class PressureServiceForDedicatedWorkerTest
rfh->GetGlobalId(), rfh->GetGlobalId(), rfh->GetStorageKey(),
rfh->GetStorageKey().origin(), rfh->GetIsolationInfoForSubresources(),
rfh->BuildClientSecurityState(), nullptr, nullptr,
mojo::PendingReceiver<blink::mojom::DedicatedWorkerHost>());
mojo::PendingReceiver<blink::mojom::DedicatedWorkerHost>(),
net::StorageAccessApiStatus::kNone);
mojo::Receiver<blink::mojom::BrowserInterfaceBroker>& bib =
worker_host_->browser_interface_broker_receiver_for_testing();
blink::mojom::BrowserInterfaceBroker* broker = bib.internal_state()->impl();

@ -230,6 +230,8 @@ void ServiceWorkerHost::CreateBlobUrlStoreProvider(
storage_partition_impl->GetBlobUrlRegistry()->AddReceiver(
version()->key(), version()->key().origin(),
GetProcessHost()->GetDeprecatedID(), std::move(receiver),
// Storage access can only be granted to dedicated workers.
base::BindRepeating([]() -> bool { return false; }),
!(GetContentClient()->browser()->IsBlobUrlPartitioningEnabled(
GetProcessHost()->GetBrowserContext())));
}

@ -81,7 +81,8 @@ DedicatedWorkerHost::DedicatedWorkerHost(
network::mojom::ClientSecurityStatePtr creator_client_security_state,
base::WeakPtr<CrossOriginEmbedderPolicyReporter> creator_coep_reporter,
base::WeakPtr<CrossOriginEmbedderPolicyReporter> ancestor_coep_reporter,
mojo::PendingReceiver<blink::mojom::DedicatedWorkerHost> host)
mojo::PendingReceiver<blink::mojom::DedicatedWorkerHost> host,
net::StorageAccessApiStatus storage_access_api_status)
: service_(service),
token_(token),
worker_process_host_(worker_process_host),
@ -101,7 +102,8 @@ DedicatedWorkerHost::DedicatedWorkerHost(
ancestor_coep_reporter_(std::move(ancestor_coep_reporter)),
code_cache_host_receivers_(GetProcessHost()
->GetStoragePartition()
->GetGeneratedCodeCacheContext()) {
->GetGeneratedCodeCacheContext()),
storage_access_api_status_(storage_access_api_status) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK(worker_process_host_);
DCHECK(worker_process_host_->IsInitializedAndNotDead());
@ -791,6 +793,15 @@ void DedicatedWorkerHost::CreateBroadcastChannelProvider(
std::move(receiver));
}
bool DedicatedWorkerHost::WasStorageAccessGranted() {
switch (storage_access_api_status_) {
case net::StorageAccessApiStatus::kAccessViaAPI:
return true;
case net::StorageAccessApiStatus::kNone:
return false;
}
}
void DedicatedWorkerHost::CreateBlobUrlStoreProvider(
mojo::PendingReceiver<blink::mojom::BlobURLStore> receiver) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
@ -800,6 +811,14 @@ void DedicatedWorkerHost::CreateBlobUrlStoreProvider(
storage_partition_impl->GetBlobUrlRegistry()->AddReceiver(
GetStorageKey(), renderer_origin_, GetProcessHost()->GetDeprecatedID(),
std::move(receiver),
base::BindRepeating(
[](base::WeakPtr<DedicatedWorkerHost> host) -> bool {
if (!host) {
return false;
}
return host->WasStorageAccessGranted();
},
weak_factory_.GetWeakPtr()),
!(GetContentClient()->browser()->IsBlobUrlPartitioningEnabled(
GetProcessHost()->GetBrowserContext())),
storage::BlobURLValidityCheckBehavior::

@ -112,7 +112,8 @@ class CONTENT_EXPORT DedicatedWorkerHost final
network::mojom::ClientSecurityStatePtr creator_client_security_state,
base::WeakPtr<CrossOriginEmbedderPolicyReporter> creator_coep_reporter,
base::WeakPtr<CrossOriginEmbedderPolicyReporter> ancestor_coep_reporter,
mojo::PendingReceiver<blink::mojom::DedicatedWorkerHost> host);
mojo::PendingReceiver<blink::mojom::DedicatedWorkerHost> host,
net::StorageAccessApiStatus storage_access_api_status);
DedicatedWorkerHost(const DedicatedWorkerHost&) = delete;
DedicatedWorkerHost& operator=(const DedicatedWorkerHost&) = delete;
@ -160,6 +161,7 @@ class CONTENT_EXPORT DedicatedWorkerHost final
mojo::PendingReceiver<blink::mojom::CodeCacheHost> receiver);
void CreateBroadcastChannelProvider(
mojo::PendingReceiver<blink::mojom::BroadcastChannelProvider> receiver);
bool WasStorageAccessGranted();
void CreateBlobUrlStoreProvider(
mojo::PendingReceiver<blink::mojom::BlobURLStore> receiver);
void CreateBucketManagerHost(
@ -430,6 +432,11 @@ class CONTENT_EXPORT DedicatedWorkerHost final
BackForwardCacheBlockingDetails bfcache_blocking_details_;
// This tracks whether the document that created this dedicated worker had
// been granted storage access when the dedicated worker was created, which
// also grants storage access to the dedicated worker.
net::StorageAccessApiStatus storage_access_api_status_;
base::WeakPtrFactory<DedicatedWorkerHost> weak_factory_{this};
};

@ -125,7 +125,8 @@ void DedicatedWorkerHostFactoryImpl::CreateWorkerHostAndStartScriptLoad(
ancestor_render_frame_host_id_, creator_storage_key_, renderer_origin,
isolation_info_, std::move(creator_client_security_state_),
std::move(creator_coep_reporter_), std::move(ancestor_coep_reporter_),
pending_remote_host.InitWithNewPipeAndPassReceiver());
pending_remote_host.InitWithNewPipeAndPassReceiver(),
storage_access_api_status);
mojo::PendingRemote<blink::mojom::BrowserInterfaceBroker> broker;
host->BindBrowserInterfaceBrokerReceiver(
broker.InitWithNewPipeAndPassReceiver());

@ -604,6 +604,8 @@ void SharedWorkerHost::CreateBlobUrlStoreProvider(
storage_partition_impl->GetBlobUrlRegistry()->AddReceiver(
GetStorageKey(), instance().renderer_origin(),
GetProcessHost()->GetDeprecatedID(), std::move(receiver),
// Storage access can only be granted to dedicated workers.
base::BindRepeating([]() -> bool { return false; }),
!(GetContentClient()->browser()->IsBlobUrlPartitioningEnabled(
GetProcessHost()->GetBrowserContext())),
storage::BlobURLValidityCheckBehavior::

@ -58,13 +58,14 @@ void BlobUrlRegistry::AddReceiver(
const url::Origin& renderer_origin,
int render_process_host_id,
mojo::PendingReceiver<blink::mojom::BlobURLStore> receiver,
base::RepeatingCallback<bool()> storage_access_check_callback,
bool partitioning_disabled_by_policy,
BlobURLValidityCheckBehavior validity_check_behavior) {
worker_receivers_.Add(
std::make_unique<storage::BlobURLStoreImpl>(
storage_key, renderer_origin, render_process_host_id, AsWeakPtr(),
validity_check_behavior, base::DoNothing(),
base::BindRepeating([]() -> bool { return false; }),
std::move(storage_access_check_callback),
partitioning_disabled_by_policy),
std::move(receiver));
}

@ -69,13 +69,16 @@ class COMPONENT_EXPORT(STORAGE_BROWSER) BlobUrlRegistry {
// Binds receivers corresponding to connections from renderer worker
// contexts and stores them in `worker_receivers_`.
void AddReceiver(const blink::StorageKey& storage_key,
const url::Origin& renderer_origin,
int render_process_host_id,
mojo::PendingReceiver<blink::mojom::BlobURLStore> receiver,
bool partitioning_disabled_by_policy = false,
BlobURLValidityCheckBehavior validity_check_behavior =
BlobURLValidityCheckBehavior::DEFAULT);
void AddReceiver(
const blink::StorageKey& storage_key,
const url::Origin& renderer_origin,
int render_process_host_id,
mojo::PendingReceiver<blink::mojom::BlobURLStore> receiver,
base::RepeatingCallback<bool()> storage_access_check_callback =
base::BindRepeating([]() -> bool { return false; }),
bool partitioning_disabled_by_policy = false,
BlobURLValidityCheckBehavior validity_check_behavior =
BlobURLValidityCheckBehavior::DEFAULT);
// Returns the receivers corresponding to renderer frame contexts for use in
// tests.

@ -1066,7 +1066,8 @@
"external/wpt/FileAPI/BlobURL/cross-partition.https.html",
"external/wpt/FileAPI/BlobURL/cross-partition-worker-creation.https.html",
"external/wpt/FileAPI/BlobURL/cross-partition-navigation.https.html",
"external/wpt/storage-access-api/storage-access-beyond-cookies.blobStorage.sub.https.window.html"
"external/wpt/storage-access-api/storage-access-beyond-cookies.blobStorage.sub.https.window.html",
"external/wpt/storage-access-api/storage-access-beyond-cookies.BlobURLDedicatedWorker.sub.https.tentative.window.html"
],
"args": ["--enable-features=BlockCrossPartitionBlobUrlFetching,EnforceNoopenerOnBlobURLNavigation"],
"expires": "Sep 1, 2025",

@ -310,6 +310,68 @@
handle_shared_worker.port.postMessage("Same-origin handle access");
break;
}
case "BlobURLDedicatedWorker": {
const fetch_unsuccessful_response = "fetch_unsuccessful";
const fetch_successful_response = "fetch_successful";
const can_blob_url_be_fetched_js = `
onmessage = async (e) => {
const blob_url = e.data;
try {
const blob = await fetch(blob_url).then(response => response.blob());
await blob.text();
postMessage("${fetch_successful_response}");
} catch(e) {
postMessage("${fetch_unsuccessful_response}");
}
};
`;
// case 1: create dedicated worker w/o granting storage access
const worker_blob_url = new Blob([can_blob_url_be_fetched_js], { type: 'text/javascript' });
const third_party_blob_url = URL.createObjectURL(worker_blob_url);
const worker_1 = new Worker(third_party_blob_url);
await MaybeSetStorageAccess("*", "*", "allowed");
const handle = await test_driver.bless("fake user interaction", () => document.requestStorageAccess({all: true}));
const worker_blob = new Blob(["potato"]);
const first_party_blob_url = handle.createObjectURL(worker_blob);
const worker_response_promise = new Promise((resolve) => {
worker_1.onmessage = (e) => { resolve(e.data) };
worker_1.postMessage(first_party_blob_url);
});
const worker_response = await worker_response_promise;
if (worker_response === fetch_unsuccessful_response) {
message = "Dedicated worker expectedly failed fetching first-party blob URL from a third-party context without granting storage access.";
} else if (worker_response === fetch_successful_response) {
message = "Dedicated worker unexpectedly fetched first-party blob URL from a third-party context without granting storage access.";
break;
}
// case 2: create dedicated worker after storage access is granted
const worker_2 = new Worker(third_party_blob_url);
const worker_response_promise2 = new Promise((resolve) => {
worker_2.onmessage = (e) => { resolve(e.data) };
worker_2.postMessage(first_party_blob_url);
});
const worker_response2 = await worker_response_promise2;
URL.revokeObjectURL(third_party_blob_url);
handle.revokeObjectURL(first_party_blob_url);
worker_2.terminate();
if (worker_response2 === fetch_unsuccessful_response) {
message = "Dedicated worker unexpectedly failed fetching first-party blob URL from a third-party context with granting storage access.";
break;
} else if (worker_response2 === fetch_successful_response) {
message = "Blob URL DedicatedWorker tests completed successfully.";
}
break;
}
case "unpartitioned": {
await MaybeSetStorageAccess("*", "*", "allowed");
await test_driver.set_permission({ name: 'storage-access' }, 'denied');

@ -133,6 +133,9 @@ window.addEventListener("message", async (e) => {
shared_worker.port.postMessage("Cross-origin handle access");
break;
}
case "BlobURLDedicatedWorker": {
break;
}
case "unpartitioned": {
const channel = handle.BroadcastChannel(id);
channel.postMessage("Cross-origin handle access");

@ -0,0 +1,5 @@
This is a testharness.js-based test.
[FAIL] Verify that if the third-party context creates a blob URL using the StorageAccessHandle and sends it to the dedicated worker, the dedicated worker fetch succeeds.
assert_equals: expected "Blob URL DedicatedWorker tests completed successfully." but got "Dedicated worker unexpectedly fetched first-party blob URL from a third-party context without granting storage access."
Harness: the test ran to completion.

@ -0,0 +1,22 @@
// META: script=/resources/testdriver.js
// META: script=/resources/testdriver-vendor.js
'use strict';
async_test(t => {
// Set up a message listener that simply calls t.done() when a message is received.
window.addEventListener("message", t.step_func(e => {
if (e.data.type != "result") {
return;
}
assert_equals(e.data.message, "Blob URL DedicatedWorker tests completed successfully.");
t.done();
}));
// Create an iframe that's cross-site with top-frame.
const id = Date.now();
let iframe = document.createElement("iframe");
iframe.src = "https://{{hosts[alt][]}}:{{ports[https][0]}}/storage-access-api/resources/storage-access-beyond-cookies-iframe.sub.html?type=BlobURLDedicatedWorker&id=" + id;
document.body.appendChild(iframe);
}, "Verify that if the third-party context creates a blob URL using the StorageAccessHandle and sends it to the dedicated worker, the dedicated worker fetch succeeds.");