mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-01-22 09:12:13 -05:00
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.
This commit is contained in:
parent
70df8122b1
commit
00cef330ef
Notes:
github-actions[bot]
2025-01-21 19:23:08 +00:00
Author: https://github.com/shannonbooth Commit: https://github.com/LadybirdBrowser/ladybird/commit/00cef330ef6 Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/3303 Reviewed-by: https://github.com/tcl3 ✅
6 changed files with 146 additions and 22 deletions
|
@ -2,7 +2,7 @@
|
||||||
* Copyright (c) 2021, Idan Horowitz <idan.horowitz@serenityos.org>
|
* Copyright (c) 2021, Idan Horowitz <idan.horowitz@serenityos.org>
|
||||||
* Copyright (c) 2021, the SerenityOS developers.
|
* Copyright (c) 2021, the SerenityOS developers.
|
||||||
* Copyright (c) 2023, networkException <networkexception@serenityos.org>
|
* Copyright (c) 2023, networkException <networkexception@serenityos.org>
|
||||||
* Copyright (c) 2024, Shannon Booth <shannon@serenityos.org>
|
* Copyright (c) 2024-2025, Shannon Booth <shannon@serenityos.org>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
* 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)
|
if (url_record.scheme() != "blob"sv)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// 3. Let origin be the origin of url record.
|
// 3. Let entry be urlRecord’s blob URL entry.
|
||||||
auto origin = url_record.origin();
|
auto& entry = url_record.blob_url_entry();
|
||||||
|
|
||||||
// 4. Let settings be the current settings object.
|
// 4. If entry is null, return.
|
||||||
auto& settings = HTML::current_principal_settings_object();
|
if (!entry.has_value())
|
||||||
|
|
||||||
// 5. If origin is not same origin with settings’s origin, return.
|
|
||||||
if (!origin.is_same_origin(settings.origin()))
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// 6. Remove an entry from the Blob URL Store for url.
|
// 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());
|
||||||
|
|
||||||
|
// 6. If isAuthorized is false, then return.
|
||||||
|
if (!is_authorized)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// 7. Remove an entry from the Blob URL Store for url.
|
||||||
FileAPI::remove_entry_from_blob_url_store(url);
|
FileAPI::remove_entry_from_blob_url_store(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
* Copyright (c) 2023, Luke Wilde <lukew@serenityos.org>
|
* Copyright (c) 2023, Luke Wilde <lukew@serenityos.org>
|
||||||
* Copyright (c) 2023, Sam Atkins <atkinssj@serenityos.org>
|
* Copyright (c) 2023, Sam Atkins <atkinssj@serenityos.org>
|
||||||
* Copyright (c) 2024, Jamie Mansfield <jmansfield@cadixdev.org>
|
* Copyright (c) 2024, Jamie Mansfield <jmansfield@cadixdev.org>
|
||||||
|
* Copyright (c) 2025, Shannon Booth <shannon@serenityos.org>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
* 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> {};
|
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
|
// https://fetch.spec.whatwg.org/#fetch-finale
|
||||||
void fetch_response_handover(JS::Realm& realm, Infrastructure::FetchParams const& fetch_params, Infrastructure::Response& response)
|
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.
|
// 1. Let blobURLEntry be request’s current URL’s blob URL entry.
|
||||||
auto const& blob_url_entry = request->current_url().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,
|
// 2. If request’s method is not `GET` or blobURLEntry is null, then return a network error. [FILEAPI]
|
||||||
// then return a network error. [FILEAPI]
|
|
||||||
if (request->method() != "GET"sv.bytes() || !blob_url_entry.has_value()) {
|
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
|
// 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.
|
// 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));
|
return PendingResponse::create(vm, request, Infrastructure::Response::network_error(vm, "Request has an invalid 'blob:' URL"sv));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Let blob be blobURLEntry’s object.
|
// 3. Let requestEnvironment be the result of determining the environment given request.
|
||||||
auto const blob = FileAPI::Blob::create(realm, blob_url_entry.value().object.data, blob_url_entry.value().object.type);
|
auto request_environment = determine_the_environment(request);
|
||||||
|
|
||||||
// 4. Let response be a new response.
|
// 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);
|
||||||
|
|
||||||
|
// 9. Let response be a new response.
|
||||||
auto response = Infrastructure::Response::create(vm);
|
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();
|
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);
|
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();
|
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())) {
|
if (!request->header_list()->contains("Range"sv.bytes())) {
|
||||||
// 1. Let bodyWithType be the result of safely extracting blob.
|
// 1. Let bodyWithType be the result of safely extracting blob.
|
||||||
auto body_with_type = safely_extract_body(realm, blob->raw_bytes());
|
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);
|
auto content_type_header = Infrastructure::Header::from_string_pair("Content-Type"sv, type);
|
||||||
response->header_list()->append(move(content_type_header));
|
response->header_list()->append(move(content_type_header));
|
||||||
}
|
}
|
||||||
// 9. Otherwise:
|
// 14. Otherwise:
|
||||||
else {
|
else {
|
||||||
// 1. Set response’s range-requested flag.
|
// 1. Set response’s range-requested flag.
|
||||||
response->set_range_requested(true);
|
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));
|
response->header_list()->append(move(content_range_header));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 10. Return response.
|
// 15. Return response.
|
||||||
return PendingResponse::create(vm, request, response);
|
return PendingResponse::create(vm, request, response);
|
||||||
}
|
}
|
||||||
// -> "data"
|
// -> "data"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
|
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
|
||||||
* Copyright (c) 2024, Andreas Kling <andreas@ladybird.org>
|
* Copyright (c) 2024, Andreas Kling <andreas@ladybird.org>
|
||||||
* Copyright (c) 2024, Shannon Booth <shannon@serenityos.org>
|
* Copyright (c) 2024-2025, Shannon Booth <shannon@serenityos.org>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
*/
|
*/
|
||||||
|
@ -14,6 +14,7 @@
|
||||||
#include <LibWeb/FileAPI/Blob.h>
|
#include <LibWeb/FileAPI/Blob.h>
|
||||||
#include <LibWeb/FileAPI/BlobURLStore.h>
|
#include <LibWeb/FileAPI/BlobURLStore.h>
|
||||||
#include <LibWeb/HTML/Scripting/Environments.h>
|
#include <LibWeb/HTML/Scripting/Environments.h>
|
||||||
|
#include <LibWeb/StorageAPI/StorageKey.h>
|
||||||
|
|
||||||
namespace Web::FileAPI {
|
namespace Web::FileAPI {
|
||||||
|
|
||||||
|
@ -78,6 +79,41 @@ ErrorOr<String> add_entry_to_blob_url_store(GC::Ref<Blob> object)
|
||||||
return url;
|
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
|
// https://w3c.github.io/FileAPI/#removeTheEntry
|
||||||
void remove_entry_from_blob_url_store(StringView url)
|
void remove_entry_from_blob_url_store(StringView url)
|
||||||
{
|
{
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
#include <AK/String.h>
|
#include <AK/String.h>
|
||||||
#include <LibGC/Ptr.h>
|
#include <LibGC/Ptr.h>
|
||||||
#include <LibGC/Root.h>
|
#include <LibGC/Root.h>
|
||||||
#include <LibURL/Forward.h>
|
#include <LibURL/URL.h>
|
||||||
#include <LibWeb/Forward.h>
|
#include <LibWeb/Forward.h>
|
||||||
|
|
||||||
namespace Web::FileAPI {
|
namespace Web::FileAPI {
|
||||||
|
@ -27,6 +27,9 @@ using BlobURLStore = HashMap<String, BlobURLEntry>;
|
||||||
BlobURLStore& blob_url_store();
|
BlobURLStore& blob_url_store();
|
||||||
ErrorOr<String> generate_new_blob_url();
|
ErrorOr<String> generate_new_blob_url();
|
||||||
ErrorOr<String> add_entry_to_blob_url_store(GC::Ref<Blob> object);
|
ErrorOr<String> add_entry_to_blob_url_store(GC::Ref<Blob> object);
|
||||||
|
bool check_for_same_partition_blob_url_usage(URL::BlobURLEntry const&, GC::Ref<HTML::Environment>);
|
||||||
|
struct NavigationEnvironment { };
|
||||||
|
Optional<URL::BlobURLEntry::Object> obtain_a_blob_object(URL::BlobURLEntry const&, Variant<GC::Ref<HTML::Environment>, NavigationEnvironment> environment);
|
||||||
void remove_entry_from_blob_url_store(StringView url);
|
void remove_entry_from_blob_url_store(StringView url);
|
||||||
Optional<BlobURLEntry const&> resolve_a_blob_url(URL::URL const&);
|
Optional<BlobURLEntry const&> resolve_a_blob_url(URL::URL const&);
|
||||||
|
|
||||||
|
|
1
Tests/LibWeb/Text/expected/FileAPI/Blob-partitioning.txt
Normal file
1
Tests/LibWeb/Text/expected/FileAPI/Blob-partitioning.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
TypeError: Failed to obtain a Blob object from 'blob:' URL
|
42
Tests/LibWeb/Text/input/FileAPI/Blob-partitioning.html
Normal file
42
Tests/LibWeb/Text/input/FileAPI/Blob-partitioning.html
Normal file
|
@ -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>
|
Loading…
Reference in a new issue