Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cherry-pick "LibWeb: Support SRI for import maps" #25258

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 13 additions & 11 deletions Userland/Libraries/LibWeb/Bindings/MainThreadVM.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -424,11 +424,11 @@ ErrorOr<void> initialize_main_thread_vm(HTML::EventLoop::Type type)
// 3. Let referencingScript be null.
Optional<HTML::Script&> referencing_script;

// FIXME: 4. Let fetchOptions be the default classic script fetch options.
auto fetch_options = HTML::default_classic_script_fetch_options();
// 4. Let originalFetchOptions be the default classic script fetch options.
auto original_fetch_options = HTML::default_classic_script_fetch_options();

// 5. Let fetchReferrer be "client".
auto fetch_referrer = Fetch::Infrastructure::Request::Referrer::Client;
Fetch::Infrastructure::Request::ReferrerType fetch_referrer = Fetch::Infrastructure::Request::Referrer::Client;

// 6. If referrer is a Script Record or a Module Record, then:
if (referrer.has<JS::NonnullGCPtr<JS::Script>>() || referrer.has<JS::NonnullGCPtr<JS::CyclicModule>>()) {
Expand All @@ -438,11 +438,10 @@ ErrorOr<void> initialize_main_thread_vm(HTML::EventLoop::Type type)
// 2. Set settingsObject to referencingScript's settings object.
settings_object = referencing_script->settings_object();

// FIXME: 3. Set fetchOptions to the new descendant script fetch options for referencingScript's fetch options.
// 3. Set fetchReferrer to referencingScript's base URL.
fetch_referrer = referencing_script->base_url();

// FIXME: 4. Assert: fetchOptions is not null, as referencingScript is a classic script or a JavaScript module script.

// FIXME: 5. Set fetchReferrer to referrer's base URL.
// FIXME: 4. Set originalFetchOptions to referencingScript's fetch options.
}

// 7. Disallow further import maps given settingsObject.
Expand All @@ -465,13 +464,16 @@ ErrorOr<void> initialize_main_thread_vm(HTML::EventLoop::Type type)
return;
}

// 10. Let destination be "script".
// 10. Let fetchOptions be the result of getting the descendant script fetch options given originalFetchOptions, url, and settingsObject.
auto fetch_options = MUST(HTML::get_descendant_script_fetch_options(original_fetch_options, url.value(), *settings_object));

// 11. Let destination be "script".
auto destination = Fetch::Infrastructure::Request::Destination::Script;

// 11. Let fetchClient be settingsObject.
// 12. Let fetchClient be settingsObject.
JS::NonnullGCPtr fetch_client { *settings_object };

// 12. If loadState is not undefined, then:
// 13. If loadState is not undefined, then:
HTML::PerformTheFetchHook perform_fetch;
if (load_state) {
auto& fetch_context = static_cast<HTML::FetchContext&>(*load_state);
Expand Down Expand Up @@ -539,7 +541,7 @@ ErrorOr<void> initialize_main_thread_vm(HTML::EventLoop::Type type)
vm.pop_execution_context();
});

// 13. Fetch a single imported module script given url, fetchClient, destination, fetchOptions, settingsObject, fetchReferrer,
// 14. Fetch a single imported module script given url, fetchClient, destination, fetchOptions, settingsObject, fetchReferrer,
// moduleRequest, and onSingleFetchComplete as defined below.
// If loadState is not undefined and loadState.[[PerformFetch]] is not null, pass loadState.[[PerformFetch]] along as well.
HTML::fetch_single_imported_module_script(realm, url.release_value(), *fetch_client, destination, fetch_options, *settings_object, fetch_referrer, module_request, perform_fetch, on_single_fetch_complete);
Expand Down
4 changes: 4 additions & 0 deletions Userland/Libraries/LibWeb/HTML/HTMLScriptElement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,10 @@ void HTMLScriptElement::prepare_script()
}
// -> "module"
else if (m_script_type == ScriptType::Module) {
// If el does not have an integrity attribute, then set options's integrity metadata to the result of resolving a module integrity metadata with url and settings object.
if (!has_attribute(HTML::AttributeNames::integrity))
options.integrity_metadata = MUST(resolve_a_module_integrity_metadata(url, settings_object));

// Fetch an external module script graph given url, settings object, options, and onComplete.
fetch_external_module_script_graph(realm(), url, settings_object, options, on_complete);
}
Expand Down
39 changes: 38 additions & 1 deletion Userland/Libraries/LibWeb/HTML/Scripting/Fetching.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,43 @@ static void set_up_module_script_request(Fetch::Infrastructure::Request& request
request.set_priority(options.fetch_priority);
}

// https://html.spec.whatwg.org/multipage/webappapis.html#get-the-descendant-script-fetch-options
WebIDL::ExceptionOr<ScriptFetchOptions> get_descendant_script_fetch_options(ScriptFetchOptions const& original_options, URL::URL const& url, EnvironmentSettingsObject& settings_object)
{
// 1. Let newOptions be a copy of originalOptions.
auto new_options = original_options;

// 2. Let integrity be the empty string.
String integrity;

// 3. If settingsObject's global object is a Window object, then set integrity to the result of resolving a module integrity metadata with url and settingsObject.
if (is<Window>(settings_object.global_object()))
integrity = TRY(resolve_a_module_integrity_metadata(url, settings_object));

// 4. Set newOptions's integrity metadata to integrity.
new_options.integrity_metadata = integrity;

// 5. Set newOptions's fetch priority to "auto".
new_options.fetch_priority = Fetch::Infrastructure::Request::Priority::Auto;

// 6. Return newOptions.
return new_options;
}

// https://html.spec.whatwg.org/multipage/webappapis.html#resolving-a-module-integrity-metadata
WebIDL::ExceptionOr<String> resolve_a_module_integrity_metadata(const URL::URL& url, EnvironmentSettingsObject& settings_object)
{
// 1. Assert: settingsObject's global object is a Window object.
VERIFY(is<Window>(settings_object.global_object()));

// 2. Let map be settingsObject's global object's import map.
auto map = static_cast<Window const&>(settings_object.global_object()).import_map();

// 3. If map's integrity[url] does not exist, then return the empty string.
// 4. Return map's integrity[url].
return MUST(String::from_byte_string(map.integrity().get(url).value_or("")));
}

// https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-classic-script
WebIDL::ExceptionOr<void> fetch_classic_script(JS::NonnullGCPtr<HTMLScriptElement> element, URL::URL const& url, EnvironmentSettingsObject& settings_object, ScriptFetchOptions options, CORSSettingAttribute cors_setting, String character_encoding, OnFetchScriptComplete on_complete)
{
Expand Down Expand Up @@ -858,7 +895,7 @@ void fetch_single_imported_module_script(JS::Realm& realm,
Fetch::Infrastructure::Request::Destination destination,
ScriptFetchOptions const& options,
EnvironmentSettingsObject& settings_object,
Fetch::Infrastructure::Request::Referrer referrer,
Fetch::Infrastructure::Request::ReferrerType referrer,
JS::ModuleRequest const& module_request,
PerformTheFetchHook perform_fetch,
OnFetchScriptComplete on_complete)
Expand Down
5 changes: 3 additions & 2 deletions Userland/Libraries/LibWeb/HTML/Scripting/Fetching.h
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ ByteString module_type_from_module_request(JS::ModuleRequest const&);
WebIDL::ExceptionOr<URL::URL> resolve_module_specifier(Optional<Script&> referring_script, ByteString const& specifier);
WebIDL::ExceptionOr<Optional<URL::URL>> resolve_imports_match(ByteString const& normalized_specifier, Optional<URL::URL> as_url, ModuleSpecifierMap const&);
Optional<URL::URL> resolve_url_like_module_specifier(ByteString const& specifier, URL::URL const& base_url);

WebIDL::ExceptionOr<ScriptFetchOptions> get_descendant_script_fetch_options(ScriptFetchOptions const& original_options, URL::URL const& url, EnvironmentSettingsObject& settings_object);
WebIDL::ExceptionOr<String> resolve_a_module_integrity_metadata(URL::URL const& url, EnvironmentSettingsObject& settings_object);
WebIDL::ExceptionOr<void> fetch_classic_script(JS::NonnullGCPtr<HTMLScriptElement>, URL::URL const&, EnvironmentSettingsObject& settings_object, ScriptFetchOptions options, CORSSettingAttribute cors_setting, String character_encoding, OnFetchScriptComplete on_complete);
WebIDL::ExceptionOr<void> fetch_classic_worker_script(URL::URL const&, EnvironmentSettingsObject& fetch_client, Fetch::Infrastructure::Request::Destination, EnvironmentSettingsObject& settings_object, PerformTheFetchHook, OnFetchScriptComplete);
WebIDL::ExceptionOr<JS::NonnullGCPtr<ClassicScript>> fetch_a_classic_worker_imported_script(URL::URL const&, HTML::EnvironmentSettingsObject&, PerformTheFetchHook = nullptr);
Expand All @@ -95,7 +96,7 @@ WebIDL::ExceptionOr<void> fetch_worklet_module_worker_script_graph(URL::URL cons
void fetch_internal_module_script_graph(JS::Realm&, JS::ModuleRequest const& module_request, EnvironmentSettingsObject& fetch_client_settings_object, Fetch::Infrastructure::Request::Destination, ScriptFetchOptions const&, Script& referring_script, HashTable<ModuleLocationTuple> const& visited_set, PerformTheFetchHook, OnFetchScriptComplete on_complete);
void fetch_external_module_script_graph(JS::Realm&, URL::URL const&, EnvironmentSettingsObject& settings_object, ScriptFetchOptions const&, OnFetchScriptComplete on_complete);
void fetch_inline_module_script_graph(JS::Realm&, ByteString const& filename, ByteString const& source_text, URL::URL const& base_url, EnvironmentSettingsObject& settings_object, OnFetchScriptComplete on_complete);
void fetch_single_imported_module_script(JS::Realm&, URL::URL const&, EnvironmentSettingsObject& fetch_client, Fetch::Infrastructure::Request::Destination, ScriptFetchOptions const&, EnvironmentSettingsObject& settings_object, Fetch::Infrastructure::Request::Referrer, JS::ModuleRequest const&, PerformTheFetchHook, OnFetchScriptComplete on_complete);
void fetch_single_imported_module_script(JS::Realm&, URL::URL const&, EnvironmentSettingsObject& fetch_client, Fetch::Infrastructure::Request::Destination, ScriptFetchOptions const&, EnvironmentSettingsObject& settings_object, Fetch::Infrastructure::Request::ReferrerType, JS::ModuleRequest const&, PerformTheFetchHook, OnFetchScriptComplete on_complete);

void fetch_descendants_of_a_module_script(JS::Realm&, JavaScriptModuleScript& module_script, EnvironmentSettingsObject& fetch_client_settings_object, Fetch::Infrastructure::Request::Destination, HashTable<ModuleLocationTuple> visited_set, PerformTheFetchHook, OnFetchScriptComplete callback);
void fetch_descendants_of_and_link_a_module_script(JS::Realm&, JavaScriptModuleScript&, EnvironmentSettingsObject&, Fetch::Infrastructure::Request::Destination, PerformTheFetchHook, OnFetchScriptComplete on_complete);
Expand Down
65 changes: 62 additions & 3 deletions Userland/Libraries/LibWeb/HTML/Scripting/ImportMap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,20 +58,36 @@ WebIDL::ExceptionOr<ImportMap> parse_import_map_string(JS::Realm& realm, ByteStr
sorted_and_normalised_scopes = TRY(sort_and_normalise_scopes(realm, scopes.as_object(), base_url));
}

// 7. If parsed's keys contains any items besides "imports" or "scopes", then the user agent should report a warning to the console indicating that an invalid top-level key was present in the import map.
// 7. Let normalizedIntegrity be an empty ordered map.
ModuleIntegrityMap normalised_integrity;

// 8. If parsed["integrity"] exists, then:
if (TRY(parsed_object.has_property("integrity"))) {
auto integrity = TRY(parsed_object.get("integrity"));

// 1. If parsed["integrity"] is not an ordered map, then throw a TypeError indicating that the value for the "integrity" top-level key needs to be a JSON object.
if (!integrity.is_object())
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, String::formatted("The 'integrity' top-level value of an importmap needs to be a JSON object.").release_value_but_fixme_should_propagate_errors() };

// 2. Set normalizedIntegrity to the result of normalizing a module integrity map given parsed["integrity"] and baseURL.
normalised_integrity = TRY(normalize_module_integrity_map(realm, integrity.as_object(), base_url));
}

// 9. If parsed's keys contains any items besides "imports", "scopes", or "integrity", then the user agent should report a warning to the console indicating that an invalid top-level key was present in the import map.
for (auto& key : parsed_object.shape().property_table().keys()) {
if (key.as_string().is_one_of("imports", "scopes"))
if (key.as_string().is_one_of("imports", "scopes", "integrity"))
continue;

auto& console = realm.intrinsics().console_object()->console();
console.output_debug_message(JS::Console::LogLevel::Warn,
TRY_OR_THROW_OOM(realm.vm(), String::formatted("An invalid top-level key ({}) was present in the import map", key.as_string())));
}

// 8. Return an import map whose imports are sortedAndNormalizedImports and whose scopes are sortedAndNormalizedScopes.
// 10. Return an import map whose imports are sortedAndNormalizedImports, whose scopes are sortedAndNormalizedScopes, and whose integrity are normalizedIntegrity.
ImportMap import_map;
import_map.set_imports(sorted_and_normalised_imports);
import_map.set_scopes(sorted_and_normalised_scopes);
import_map.set_integrity(normalised_integrity);
return import_map;
}

Expand Down Expand Up @@ -209,4 +225,47 @@ WebIDL::ExceptionOr<HashMap<URL::URL, ModuleSpecifierMap>> sort_and_normalise_sc
return normalised;
}

// https://html.spec.whatwg.org/multipage/webappapis.html#normalizing-a-module-integrity-map
WebIDL::ExceptionOr<ModuleIntegrityMap> normalize_module_integrity_map(JS::Realm& realm, JS::Object& original_map, URL::URL base_url)
{
// 1. Let normalized be an empty ordered map.
ModuleIntegrityMap normalised;

// 2. For each key → value of originalMap:
for (auto& key : original_map.shape().property_table().keys()) {
auto value = TRY(original_map.get(key.as_string()));

// 1. Let resolvedURL be the result of resolving a URL-like module specifier given key and baseURL.
auto resolved_url = resolve_url_like_module_specifier(key.as_string(), base_url);

// 2. If resolvedURL is null, then:
if (!resolved_url.has_value()) {
// 1. The user agent may report a warning to the console indicating that the key failed to resolve.
auto& console = realm.intrinsics().console_object()->console();
console.output_debug_message(JS::Console::LogLevel::Warn,
TRY_OR_THROW_OOM(realm.vm(), String::formatted("Failed to resolve key ({})", key.as_string())));

// 2. Continue.
continue;
}

// 3. If value is not a string, then:
if (!value.is_string()) {
// 1. The user agent may report a warning to the console indicating that integrity metadata values need to be strings.
auto& console = realm.intrinsics().console_object()->console();
console.output_debug_message(JS::Console::LogLevel::Warn,
TRY_OR_THROW_OOM(realm.vm(), String::formatted("Integrity metadata value for '{}' needs to be a string", key.as_string())));

// 2. Continue.
continue;
}

// 4. Set normalized[resolvedURL] to value.
normalised.set(resolved_url.release_value(), value.as_string().byte_string());
}

// 3. Return normalized.
return normalised;
}

}
7 changes: 7 additions & 0 deletions Userland/Libraries/LibWeb/HTML/Scripting/ImportMap.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
namespace Web::HTML {

using ModuleSpecifierMap = HashMap<ByteString, Optional<URL::URL>>;
using ModuleIntegrityMap = HashMap<URL::URL, ByteString>;

// https://html.spec.whatwg.org/multipage/webappapis.html#import-map
class ImportMap {
Expand All @@ -28,14 +29,20 @@ class ImportMap {
HashMap<URL::URL, ModuleSpecifierMap>& scopes() { return m_scopes; }
void set_scopes(HashMap<URL::URL, ModuleSpecifierMap> const& scopes) { m_scopes = scopes; }

ModuleIntegrityMap const& integrity() const { return m_integrity; }
ModuleIntegrityMap integrity() { return m_integrity; }
void set_integrity(ModuleIntegrityMap const& integrity) { m_integrity = integrity; }

private:
ModuleSpecifierMap m_imports;
HashMap<URL::URL, ModuleSpecifierMap> m_scopes;
ModuleIntegrityMap m_integrity;
};

WebIDL::ExceptionOr<ImportMap> parse_import_map_string(JS::Realm& realm, ByteString const& input, URL::URL base_url);
WebIDL::ExceptionOr<Optional<DeprecatedFlyString>> normalise_specifier_key(JS::Realm& realm, DeprecatedFlyString specifier_key, URL::URL base_url);
WebIDL::ExceptionOr<ModuleSpecifierMap> sort_and_normalise_module_specifier_map(JS::Realm& realm, JS::Object& original_map, URL::URL base_url);
WebIDL::ExceptionOr<HashMap<URL::URL, ModuleSpecifierMap>> sort_and_normalise_scopes(JS::Realm& realm, JS::Object& original_map, URL::URL base_url);
WebIDL::ExceptionOr<ModuleIntegrityMap> normalize_module_integrity_map(JS::Realm& realm, JS::Object& original_map, URL::URL base_url);

}
Loading