-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
LibWeb: Partition Blob URL fetches by Storage Key
This was a security mechanism introduced in the fetch spec, with supporting AOs added to the FileAPI spec.
- Loading branch information
1 parent
70df812
commit 00cef33
Showing
6 changed files
with
145 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,7 +2,7 @@ | |
* Copyright (c) 2021, Idan Horowitz <[email protected]> | ||
* Copyright (c) 2021, the SerenityOS developers. | ||
* Copyright (c) 2023, networkException <[email protected]> | ||
* Copyright (c) 2024, Shannon Booth <[email protected]> | ||
* Copyright (c) 2024-2025, Shannon Booth <[email protected]> | ||
* | ||
* SPDX-License-Identifier: BSD-2-Clause | ||
*/ | ||
|
@@ -136,17 +136,21 @@ void DOMURL::revoke_object_url(JS::VM&, StringView url) | |
if (url_record.scheme() != "blob"sv) | ||
return; | ||
|
||
// 3. Let origin be the origin of url record. | ||
auto origin = url_record.origin(); | ||
// 3. Let entry be urlRecord’s blob URL entry. | ||
auto& entry = url_record.blob_url_entry(); | ||
|
||
// 4. Let settings be the current settings object. | ||
auto& settings = HTML::current_principal_settings_object(); | ||
// 4. If entry is null, return. | ||
if (!entry.has_value()) | ||
return; | ||
|
||
// 5. Let isAuthorized be the result of checking for same-partition blob URL usage with entry and the current settings object. | ||
bool is_authorized = FileAPI::check_for_same_partition_blob_url_usage(entry.value(), HTML::current_principal_settings_object()); | ||
|
||
// 5. If origin is not same origin with settings’s origin, return. | ||
if (!origin.is_same_origin(settings.origin())) | ||
// 6. If isAuthorized is false, then return. | ||
if (!is_authorized) | ||
return; | ||
|
||
// 6. Remove an entry from the Blob URL Store for url. | ||
// 7. Remove an entry from the Blob URL Store for url. | ||
FileAPI::remove_entry_from_blob_url_store(url); | ||
} | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,7 @@ | |
* Copyright (c) 2023, Luke Wilde <[email protected]> | ||
* Copyright (c) 2023, Sam Atkins <[email protected]> | ||
* Copyright (c) 2024, Jamie Mansfield <[email protected]> | ||
* Copyright (c) 2025, Shannon Booth <[email protected]> | ||
* | ||
* SPDX-License-Identifier: BSD-2-Clause | ||
*/ | ||
|
@@ -595,6 +596,21 @@ WebIDL::ExceptionOr<GC::Ptr<PendingResponse>> main_fetch(JS::Realm& realm, Infra | |
return GC::Ptr<PendingResponse> {}; | ||
} | ||
|
||
// https://fetch.spec.whatwg.org/#request-determine-the-environment | ||
static GC::Ptr<HTML::Environment> determine_the_environment(GC::Ref<Infrastructure::Request> request) | ||
{ | ||
// 1. If request’s reserved client is non-null, then return request’s reserved client. | ||
if (request->reserved_client()) | ||
return request->reserved_client(); | ||
|
||
// 2. If request’s client is non-null, then return request’s client. | ||
if (request->client()) | ||
return request->client(); | ||
|
||
// 3. Return null. | ||
return {}; | ||
} | ||
|
||
// https://fetch.spec.whatwg.org/#fetch-finale | ||
void fetch_response_handover(JS::Realm& realm, Infrastructure::FetchParams const& fetch_params, Infrastructure::Response& response) | ||
{ | ||
|
@@ -817,30 +833,52 @@ WebIDL::ExceptionOr<GC::Ref<PendingResponse>> scheme_fetch(JS::Realm& realm, Inf | |
// 1. Let blobURLEntry be request’s current URL’s blob URL entry. | ||
auto const& blob_url_entry = request->current_url().blob_url_entry(); | ||
|
||
// 2. If request’s method is not `GET`, blobURLEntry is null, or blobURLEntry’s object is not a Blob object, | ||
// then return a network error. [FILEAPI] | ||
// 2. If request’s method is not `GET` or blobURLEntry is null, then return a network error. [FILEAPI] | ||
if (request->method() != "GET"sv.bytes() || !blob_url_entry.has_value()) { | ||
// FIXME: Handle "blobURLEntry’s object is not a Blob object". It could be a MediaSource object, but we | ||
// have not yet implemented the Media Source Extensions spec. | ||
return PendingResponse::create(vm, request, Infrastructure::Response::network_error(vm, "Request has an invalid 'blob:' URL"sv)); | ||
} | ||
|
||
// 3. Let blob be blobURLEntry’s object. | ||
auto const blob = FileAPI::Blob::create(realm, blob_url_entry.value().object.data, blob_url_entry.value().object.type); | ||
// 3. Let requestEnvironment be the result of determining the environment given request. | ||
auto request_environment = determine_the_environment(request); | ||
|
||
// 4. Let isTopLevelNavigation be true if request’s destination is "document"; otherwise, false. | ||
bool is_top_level_navigation = request->destination() == Infrastructure::Request::Destination::Document; | ||
|
||
// 5. If isTopLevelNavigation is false and requestEnvironment is null, then return a network error. | ||
if (!is_top_level_navigation && !request_environment) | ||
return PendingResponse::create(vm, request, Infrastructure::Response::network_error(vm, "Request is missing fetch client"sv)); | ||
|
||
// 6. Let navigationOrEnvironment be the string "navigation" if isTopLevelNavigation is true; otherwise, requestEnvironment. | ||
auto navigation_or_environment = [&]() -> Variant<FileAPI::NavigationEnvironment, GC::Ref<HTML::Environment>> { | ||
if (is_top_level_navigation) | ||
return FileAPI::NavigationEnvironment {}; | ||
return GC::Ref { *request_environment }; | ||
}(); | ||
|
||
// 7. Let blob be the result of obtaining a blob object given blobURLEntry and navigationOrEnvironment. | ||
auto blob_object = FileAPI::obtain_a_blob_object(blob_url_entry.value(), navigation_or_environment); | ||
|
||
// 8. If blob is not a Blob object, then return a network error. | ||
// FIXME: This should probably check for a MediaSource object as well, once we implement that. | ||
if (!blob_object.has_value()) | ||
return PendingResponse::create(vm, request, Infrastructure::Response::network_error(vm, "Failed to obtain a Blob object from 'blob:' URL"sv)); | ||
auto const blob = FileAPI::Blob::create(realm, blob_object->data, blob_object->type); | ||
|
||
// 4. Let response be a new response. | ||
// 9. Let response be a new response. | ||
auto response = Infrastructure::Response::create(vm); | ||
|
||
// 5. Let fullLength be blob’s size. | ||
// 10. Let fullLength be blob’s size. | ||
auto full_length = blob->size(); | ||
|
||
// 6. Let serializedFullLength be fullLength, serialized and isomorphic encoded. | ||
// 11. Let serializedFullLength be fullLength, serialized and isomorphic encoded. | ||
auto serialized_full_length = String::number(full_length); | ||
|
||
// 7. Let type be blob’s type. | ||
// 12. Let type be blob’s type. | ||
auto const& type = blob->type(); | ||
|
||
// 8. If request’s header list does not contain `Range`: | ||
// 13. If request’s header list does not contain `Range`: | ||
if (!request->header_list()->contains("Range"sv.bytes())) { | ||
// 1. Let bodyWithType be the result of safely extracting blob. | ||
auto body_with_type = safely_extract_body(realm, blob->raw_bytes()); | ||
|
@@ -858,7 +896,7 @@ WebIDL::ExceptionOr<GC::Ref<PendingResponse>> scheme_fetch(JS::Realm& realm, Inf | |
auto content_type_header = Infrastructure::Header::from_string_pair("Content-Type"sv, type); | ||
response->header_list()->append(move(content_type_header)); | ||
} | ||
// 9. Otherwise: | ||
// 14. Otherwise: | ||
else { | ||
// 1. Set response’s range-requested flag. | ||
response->set_range_requested(true); | ||
|
@@ -933,7 +971,7 @@ WebIDL::ExceptionOr<GC::Ref<PendingResponse>> scheme_fetch(JS::Realm& realm, Inf | |
response->header_list()->append(move(content_range_header)); | ||
} | ||
|
||
// 10. Return response. | ||
// 15. Return response. | ||
return PendingResponse::create(vm, request, response); | ||
} | ||
// -> "data" | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
/* | ||
* Copyright (c) 2023, Tim Flynn <[email protected]> | ||
* Copyright (c) 2024, Andreas Kling <[email protected]> | ||
* Copyright (c) 2024, Shannon Booth <[email protected]> | ||
* Copyright (c) 2024-2025, Shannon Booth <[email protected]> | ||
* | ||
* SPDX-License-Identifier: BSD-2-Clause | ||
*/ | ||
|
@@ -14,6 +14,7 @@ | |
#include <LibWeb/FileAPI/Blob.h> | ||
#include <LibWeb/FileAPI/BlobURLStore.h> | ||
#include <LibWeb/HTML/Scripting/Environments.h> | ||
#include <LibWeb/StorageAPI/StorageKey.h> | ||
|
||
namespace Web::FileAPI { | ||
|
||
|
@@ -78,6 +79,41 @@ ErrorOr<String> add_entry_to_blob_url_store(GC::Ref<Blob> object) | |
return url; | ||
} | ||
|
||
// https://www.w3.org/TR/FileAPI/#check-for-same-partition-blob-url-usage | ||
bool check_for_same_partition_blob_url_usage(URL::BlobURLEntry const& blob_url_entry, GC::Ref<HTML::Environment> environment) | ||
{ | ||
// 1. Let blobStorageKey be the result of obtaining a storage key for non-storage purposes with blobUrlEntry’s environment. | ||
auto blob_storage_key = StorageAPI::obtain_a_storage_key_for_non_storage_purposes(blob_url_entry.environment.origin); | ||
|
||
// 2. Let environmentStorageKey be the result of obtaining a storage key for non-storage purposes with environment. | ||
auto environment_storage_key = StorageAPI::obtain_a_storage_key_for_non_storage_purposes(environment); | ||
|
||
// 3. If blobStorageKey is not equal to environmentStorageKey, then return false. | ||
if (blob_storage_key != environment_storage_key) | ||
return false; | ||
|
||
// 4. Return true. | ||
return true; | ||
} | ||
|
||
// https://www.w3.org/TR/FileAPI/#blob-url-obtain-object | ||
Optional<URL::BlobURLEntry::Object> obtain_a_blob_object(URL::BlobURLEntry const& blob_url_entry, Variant<GC::Ref<HTML::Environment>, NavigationEnvironment> environment) | ||
{ | ||
// 1. Let isAuthorized be true. | ||
bool is_authorized = true; | ||
|
||
// 2. If environment is not the string "navigation", then set isAuthorized to the result of checking for same-partition blob URL usage with blobUrlEntry and environment. | ||
if (!environment.has<NavigationEnvironment>()) | ||
is_authorized = check_for_same_partition_blob_url_usage(blob_url_entry, environment.get<GC::Ref<HTML::Environment>>()); | ||
|
||
// 3. If isAuthorized is false, then return failure. | ||
if (!is_authorized) | ||
return {}; | ||
|
||
// 4. Return blobUrlEntry’s object. | ||
return blob_url_entry.object; | ||
} | ||
|
||
// https://w3c.github.io/FileAPI/#removeTheEntry | ||
void remove_entry_from_blob_url_store(StringView url) | ||
{ | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
TypeError: Failed to obtain a Blob object from 'blob:' URL |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
<script src="../include.js"></script> | ||
<script> | ||
asyncTest(async (done) => { | ||
try { | ||
const httpServer = httpTestServer(); | ||
const url = await httpServer.createEcho("GET", "/blob-partitioned-fetched", { | ||
status: 200, | ||
headers: { | ||
"Access-Control-Allow-Origin": "*", | ||
}, | ||
body: ` | ||
<script> | ||
const blob = new Blob(["Hello, world!"], { type: "text/plain" }); | ||
const blobURL = URL.createObjectURL(blob); | ||
window.parent.postMessage(blobURL, "*"); | ||
<\/script> | ||
` | ||
}); | ||
|
||
const options = { | ||
method: 'GET', | ||
mode: 'no-cors' | ||
}; | ||
window.addEventListener("message", async (event) => { | ||
const blobURL = event.data; | ||
try { | ||
const response = await fetch(blobURL, options); | ||
} catch (e) { | ||
println(e); | ||
} | ||
done(); | ||
}); | ||
|
||
const iframe = document.getElementById("testIframe"); | ||
iframe.src = url; | ||
|
||
} catch (err) { | ||
console.log("FAIL - " + err); | ||
} | ||
}); | ||
</script> | ||
<iframe id="testIframe" src="about:blank"></iframe> |