mirror of
https://github.com/lexmount/moli.git
synced 2026-10-01 16:00:44 +00:00
fix(file-api): authorize blob URL revocation by storage key
Record the URL creator's storage key and browser partition independently of its backing Blob, and check authorization atomically with URL removal. Use the Worker's environment key and share opaque-origin nonce allocation between Windows and Workers so unrelated opaque origins cannot collide. Validated with cargo fmt, workspace clippy, and nextest (17990 passed, 13 skipped). Cross-global revoke WPT passes 3/3; all 874 contract checks pass, with no regressions across the 68-case WPT suite.
This commit is contained in:
@@ -405,10 +405,22 @@ mod tests {
|
||||
let first_key = "first URL creator".to_owned();
|
||||
let second_key = "second URL creator".to_owned();
|
||||
let first = store
|
||||
.create_object_url_with_access_key(Some(2), blob, "null", Some(first_key.clone()))
|
||||
.create_object_url_with_lifetime_and_access_key(
|
||||
Some(2),
|
||||
Some(101),
|
||||
blob,
|
||||
"null",
|
||||
Some(first_key.clone()),
|
||||
)
|
||||
.unwrap();
|
||||
let second = store
|
||||
.create_object_url_with_access_key(Some(3), blob, "null", Some(second_key.clone()))
|
||||
.create_object_url_with_lifetime_and_access_key(
|
||||
Some(3),
|
||||
Some(101),
|
||||
blob,
|
||||
"null",
|
||||
Some(second_key.clone()),
|
||||
)
|
||||
.unwrap();
|
||||
let unkeyed = store.create_object_url(Some(1), blob, "null").unwrap();
|
||||
store.release_blob_wrapper_ref(blob);
|
||||
@@ -427,7 +439,7 @@ mod tests {
|
||||
);
|
||||
assert!(store.revoke_object_url(&unkeyed));
|
||||
assert!(store.blob_bytes(blob).is_some());
|
||||
assert!(store.revoke_object_url_with_access_key(&second, &second_key));
|
||||
assert_eq!(store.cleanup_object_url_lifetime(3, 101), 1);
|
||||
assert!(store.blob_bytes(blob).is_none());
|
||||
assert!(!store.revoke_object_url_with_access_key(&second, &second_key));
|
||||
}
|
||||
|
||||
@@ -384,7 +384,10 @@ pub(super) fn create_object_url_for_object<'s>(
|
||||
lifetime_id,
|
||||
blob_id,
|
||||
&origin,
|
||||
Some(ObjectUrlAccessKey { partition, storage_key }),
|
||||
Some(ObjectUrlAccessKey {
|
||||
partition,
|
||||
storage_key,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -4545,6 +4545,64 @@ async fn request_init_exceptions_preserve_identity_without_fetching_or_consuming
|
||||
}).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn blob_url_revocation_uses_window_and_worker_creator_storage_keys() {
|
||||
run_page_vm_async_test(async move {
|
||||
for document_url in ["https://example.com/", "data:text/html,opaque-parent"] {
|
||||
let mut page_vm = test_page_vm_with_document_url(Url::parse(document_url).unwrap());
|
||||
let local_executor = page_vm.local_executor.clone();
|
||||
let result = local_executor.run(async move {
|
||||
page_vm.vm_mut().eval(r#"
|
||||
globalThis.__revocationResult = 'pending';
|
||||
(async () => {
|
||||
const check = (value, message) => { if (!value) throw new Error(message); };
|
||||
const source = `onmessage = async event => {
|
||||
const {action, url} = event.data;
|
||||
if (action === 'create') postMessage(URL.createObjectURL(new Blob(['payload'])));
|
||||
if (action === 'revoke') { URL.revokeObjectURL(url); postMessage('done'); }
|
||||
if (action === 'read') {
|
||||
try { postMessage(await (await fetch(url)).text()); }
|
||||
catch (error) { postMessage(error.name); }
|
||||
}
|
||||
};`;
|
||||
const sourceUrl = URL.createObjectURL(new Blob([source]));
|
||||
const workers = [new Worker(sourceUrl), new Worker('data:text/javascript,' + encodeURIComponent(source))];
|
||||
const rpc = (worker, action, url) => new Promise((resolve, reject) => {
|
||||
worker.onmessage = event => resolve(event.data);
|
||||
worker.onerror = event => reject(new Error(event.message));
|
||||
worker.postMessage({action, url});
|
||||
});
|
||||
try {
|
||||
for (let i = 0; i < workers.length; i++) {
|
||||
const worker = workers[i];
|
||||
const url = URL.createObjectURL(new Blob(['payload']));
|
||||
await rpc(worker, 'revoke', url);
|
||||
let body;
|
||||
try { body = await (await fetch(url)).text(); }
|
||||
catch (error) { body = error.name; }
|
||||
check(body === (i === 0 ? 'TypeError' : 'payload'), 'worker revocation authority');
|
||||
URL.revokeObjectURL(url);
|
||||
const childUrl = await rpc(worker, 'create');
|
||||
URL.revokeObjectURL(childUrl);
|
||||
check(await rpc(worker, 'read', childUrl) === (i === 0 ? 'TypeError' : 'payload'), 'parent revocation authority');
|
||||
await rpc(worker, 'revoke', childUrl);
|
||||
check(await rpc(worker, 'read', childUrl) === 'TypeError', 'worker can revoke its own opaque URL');
|
||||
}
|
||||
} finally {
|
||||
for (const worker of workers) worker.terminate();
|
||||
URL.revokeObjectURL(sourceUrl);
|
||||
}
|
||||
return 'ok';
|
||||
})().then(value => { globalThis.__revocationResult = value; }, error => { globalThis.__revocationResult = String(error); });
|
||||
"#)?;
|
||||
drive_websocket_until_done(&mut page_vm, "String(globalThis.__revocationResult !== 'pending')", "revocation checks should finish").await?;
|
||||
page_vm.vm_mut().eval("globalThis.__revocationResult")
|
||||
}).await.expect("blob revocation checks should run on owner lane");
|
||||
assert_eq!(result, "ok", "document_url={document_url}");
|
||||
}
|
||||
}).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn blob_url_entries_survive_request_cloning_and_xhr_open_in_window_and_worker() {
|
||||
run_page_vm_async_test(async move {
|
||||
|
||||
Reference in New Issue
Block a user