diff --git a/Libraries/LibJS/Runtime/VM.h b/Libraries/LibJS/Runtime/VM.h index 944630adfd1..7e3f24af597 100644 --- a/Libraries/LibJS/Runtime/VM.h +++ b/Libraries/LibJS/Runtime/VM.h @@ -49,6 +49,7 @@ enum class CompilationType { DirectEval, IndirectEval, Function, + Timer, }; class JS_API VM : public RefCounted { diff --git a/Libraries/LibWeb/Bindings/MainThreadVM.cpp b/Libraries/LibWeb/Bindings/MainThreadVM.cpp index a9eead814cb..bd7b114678d 100644 --- a/Libraries/LibWeb/Bindings/MainThreadVM.cpp +++ b/Libraries/LibWeb/Bindings/MainThreadVM.cpp @@ -25,6 +25,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -114,7 +117,19 @@ void initialize_main_thread_vm(AgentType type) return {}; }; - // FIXME: Implement 8.1.5.2 HostEnsureCanCompileStrings(callerRealm, calleeRealm), https://html.spec.whatwg.org/multipage/webappapis.html#hostensurecancompilestrings(callerrealm,-calleerealm) + // 8.1.6.2 HostEnsureCanCompileStrings(realm, parameterStrings, bodyString, codeString, compilationType, parameterArgs, bodyArg), https://html.spec.whatwg.org/multipage/webappapis.html#hostensurecancompilestrings(realm,-parameterstrings,-bodystring,-codestring,-compilationtype,-parameterargs,-bodyarg) + s_main_thread_vm->host_ensure_can_compile_strings = [](JS::Realm& realm, ReadonlySpan parameter_strings, StringView body_string, StringView code_string, JS::CompilationType compilation_type, ReadonlySpan parameter_args, JS::Value body_arg) -> JS::ThrowCompletionOr { + // 1. Perform ? EnsureCSPDoesNotBlockStringCompilation(realm, parameterStrings, bodyString, codeString, compilationType, parameterArgs, bodyArg). [CSP] + return ContentSecurityPolicy::ensure_csp_does_not_block_string_compilation(realm, parameter_strings, body_string, code_string, compilation_type, parameter_args, body_arg); + }; + + // 8.1.6.3 HostGetCodeForEval(argument), https://html.spec.whatwg.org/multipage/webappapis.html#hostgetcodeforeval(argument) + s_main_thread_vm->host_get_code_for_eval = [](JS::Object const&) -> GC::Ptr { + // FIXME: 1. If argument is a TrustedScript object, then return argument's data. + + // 2. Otherwise, return no-code. + return {}; + }; // 8.1.5.3 HostPromiseRejectionTracker(promise, operation), https://html.spec.whatwg.org/multipage/webappapis.html#the-hostpromiserejectiontracker-implementation // https://whatpr.org/html/9893/webappapis.html#the-hostpromiserejectiontracker-implementation diff --git a/Libraries/LibWeb/CMakeLists.txt b/Libraries/LibWeb/CMakeLists.txt index 174f243e8ac..33b5f61f2e6 100644 --- a/Libraries/LibWeb/CMakeLists.txt +++ b/Libraries/LibWeb/CMakeLists.txt @@ -55,6 +55,7 @@ set(SOURCES ContentSecurityPolicy/Directives/MediaSourceDirective.cpp ContentSecurityPolicy/Directives/Names.cpp ContentSecurityPolicy/Directives/ObjectSourceDirective.cpp + ContentSecurityPolicy/Directives/ScriptSourceDirective.cpp ContentSecurityPolicy/Directives/SerializedDirective.cpp ContentSecurityPolicy/Directives/SourceExpression.cpp ContentSecurityPolicy/Policy.cpp diff --git a/Libraries/LibWeb/ContentSecurityPolicy/BlockingAlgorithms.cpp b/Libraries/LibWeb/ContentSecurityPolicy/BlockingAlgorithms.cpp index 7cfdda7137f..1bc67cbc08c 100644 --- a/Libraries/LibWeb/ContentSecurityPolicy/BlockingAlgorithms.cpp +++ b/Libraries/LibWeb/ContentSecurityPolicy/BlockingAlgorithms.cpp @@ -6,11 +6,16 @@ #include #include +#include #include #include #include +#include +#include #include #include +#include +#include namespace Web::ContentSecurityPolicy { @@ -322,4 +327,233 @@ Directives::Directive::Result should_navigation_response_to_navigation_request_o return result; } +// https://w3c.github.io/webappsec-csp/#should-block-inline +Directives::Directive::Result should_elements_inline_type_behavior_be_blocked_by_content_security_policy(JS::Realm& realm, GC::Ref element, Directives::Directive::InlineType type, String const& source) +{ + // Spec Note: The valid values for type are "script", "script attribute", "style", and "style attribute". + VERIFY(type == Directives::Directive::InlineType::Script || type == Directives::Directive::InlineType::ScriptAttribute || type == Directives::Directive::InlineType::Style || type == Directives::Directive::InlineType::StyleAttribute); + + // 1. Assert: element is not null. + // NOTE: Already done by only accepting a GC::Ref. + + // 2. Let result be "Allowed". + auto result = Directives::Directive::Result::Allowed; + + // 3. For each policy of element’s Document's global object’s CSP list: + auto& global_object = element->document().realm().global_object(); + auto csp_list = PolicyList::from_object(global_object); + VERIFY(csp_list); + + for (auto const policy : csp_list->policies()) { + // 1. For each directive of policy’s directive set: + for (auto const directive : policy->directives()) { + // 1. If directive’s inline check returns "Allowed" when executed upon element, type, policy and source, + // skip to the next directive. + if (directive->inline_check(realm.heap(), element, type, policy, source) == Directives::Directive::Result::Allowed) + continue; + + // 2. Let directive-name be the result of executing § 6.8.2 Get the effective directive for inline checks + // on type. + auto directive_name = Directives::get_the_effective_directive_for_inline_checks(type); + + // 3. Otherwise, let violation be the result of executing § 2.4.1 Create a violation object for global, + // policy, and directive on the current settings object’s global object, policy, and directive-name. + // FIXME: File spec issue about using "current settings object" here, as it can run outside of a script + // context (for example, a just parsed inline script being prepared) + auto violation = Violation::create_a_violation_object_for_global_policy_and_directive(realm, global_object, policy, directive_name.to_string()); + + // 4. Set violation’s resource to "inline". + violation->set_resource(Violation::Resource::Inline); + + // 5. Set violation’s element to element. + violation->set_element(element); + + // 6. If directive’s value contains the expression "'report-sample'", then set violation’s sample to the + // substring of source containing its first 40 characters. + // FIXME: Should this be case insensitive? + auto maybe_report_sample = directive->value().find_if([](auto const& directive_value) { + return directive_value.equals_ignoring_ascii_case(Directives::KeywordSources::ReportSample); + }); + + if (!maybe_report_sample.is_end()) { + Utf8View source_view { source }; + auto sample = source_view.unicode_substring_view(0, min(source_view.length(), 40)); + violation->set_sample(String::from_utf8_without_validation(sample.as_string().bytes())); + } + + // 7. Execute § 5.5 Report a violation on violation. + violation->report_a_violation(realm); + + // 8. If policy’s disposition is "enforce", then set result to "Blocked". + if (policy->disposition() == Policy::Disposition::Enforce) { + result = Directives::Directive::Result::Blocked; + } + } + } + + // 4. Return result. + return result; +} + +// https://w3c.github.io/webappsec-csp/#can-compile-strings +JS::ThrowCompletionOr ensure_csp_does_not_block_string_compilation(JS::Realm& realm, ReadonlySpan, StringView, StringView code_string, JS::CompilationType, ReadonlySpan, JS::Value) +{ + // FIXME: 1. If compilationType is "TIMER", then: + // 1. Let sourceString be codeString. + StringView source_string = code_string; + // FIXME: 2. Else: + // FIXME: We don't do these two steps as we don't currently support Trusted Types. + + // 3. Let result be "Allowed". + auto result = Directives::Directive::Result::Allowed; + + // 4. Let global be realm’s global object. + auto& global = realm.global_object(); + + // 5. For each policy of global’s CSP list: + auto csp_list = PolicyList::from_object(global); + VERIFY(csp_list); + for (auto const policy : csp_list->policies()) { + // 1. Let source-list be null. + Optional> maybe_source_list; + + // 2. If policy contains a directive whose name is "script-src", then set source-list to that directive's value. + auto maybe_script_src = policy->directives().find_if([](auto const& directive) { + return directive->name() == Directives::Names::ScriptSrc; + }); + + if (!maybe_script_src.is_end()) { + maybe_source_list = (*maybe_script_src)->value(); + } else { + // Otherwise if policy contains a directive whose name is "default-src", then set source-list to that + // directive’s value. + auto maybe_default_src = policy->directives().find_if([](auto const& directive) { + return directive->name() == Directives::Names::DefaultSrc; + }); + + if (!maybe_default_src.is_end()) + maybe_source_list = (*maybe_default_src)->value(); + } + + // 3. If source-list is not null, and does not contain a source expression which is an ASCII case-insensitive + // match for the string "'unsafe-eval'", then: + if (maybe_source_list.has_value()) { + auto const& source_list = maybe_source_list.value(); + + auto maybe_unsafe_eval = source_list.find_if([](auto const& directive_value) { + return directive_value.equals_ignoring_ascii_case(Directives::KeywordSources::UnsafeEval); + }); + + if (maybe_unsafe_eval.is_end()) { + // 1. Let violation be the result of executing § 2.4.1 Create a violation object for global, policy, + // and directive on global, policy, and "script-src". + auto script_src_string = Directives::Names::ScriptSrc.to_string(); + auto violation = Violation::create_a_violation_object_for_global_policy_and_directive(realm, global, policy, script_src_string); + + // 2. Set violation’s resource to "eval". + violation->set_resource(Violation::Resource::Eval); + + // 3. If source-list contains the expression "'report-sample'", then set violation’s sample to the + // substring of sourceString containing its first 40 characters. + // FIXME: Should this be case insensitive? + auto maybe_report_sample = source_list.find_if([](auto const& directive_value) { + return directive_value.equals_ignoring_ascii_case(Directives::KeywordSources::ReportSample); + }); + + if (!maybe_report_sample.is_end()) { + Utf8View source_view { source_string }; + auto sample = source_view.unicode_substring_view(0, min(source_view.length(), 40)); + violation->set_sample(String::from_utf8_without_validation(sample.as_string().bytes())); + } + + // 4. Execute § 5.5 Report a violation on violation. + violation->report_a_violation(realm); + + // 5. If policy’s disposition is "enforce", then set result to "Blocked". + if (policy->disposition() == Policy::Disposition::Enforce) + result = Directives::Directive::Result::Blocked; + } + } + } + + // 6. If result is "Blocked", throw an EvalError exception. + if (result == Directives::Directive::Result::Blocked) { + return realm.vm().throw_completion("Blocked by Content Security Policy"sv); + } + + return {}; +} + +// https://w3c.github.io/webappsec-csp/#can-compile-wasm-bytes +JS::ThrowCompletionOr ensure_csp_does_not_block_wasm_byte_compilation(JS::Realm& realm) +{ + // 1. Let global be realm’s global object. + auto& global = realm.global_object(); + + // 2. Let result be "Allowed". + auto result = Directives::Directive::Result::Allowed; + + // 3. For each policy of global’s CSP list: + auto csp_list = PolicyList::from_object(global); + VERIFY(csp_list); + for (auto const policy : csp_list->policies()) { + // 1. Let source-list be null. + Optional> maybe_source_list; + + // 2. If policy contains a directive whose name is "script-src", then set source-list to that directive's value. + auto maybe_script_src = policy->directives().find_if([](auto const& directive) { + return directive->name() == Directives::Names::ScriptSrc; + }); + + if (!maybe_script_src.is_end()) { + maybe_source_list = (*maybe_script_src)->value(); + } else { + // Otherwise if policy contains a directive whose name is "default-src", then set source-list to that + // directive’s value. + auto maybe_default_src = policy->directives().find_if([](auto const& directive) { + return directive->name() == Directives::Names::DefaultSrc; + }); + + if (!maybe_default_src.is_end()) + maybe_source_list = (*maybe_default_src)->value(); + } + + // 3. If source-list is non-null, and does not contain a source expression which is an ASCII case-insensitive + // match for the string "'unsafe-eval'", and does not contain a source expression which is an ASCII + // case-insensitive match for the string "'wasm-unsafe-eval'", then: + if (maybe_source_list.has_value()) { + auto const& source_list = maybe_source_list.value(); + + auto maybe_unsafe_eval = source_list.find_if([](auto const& directive_value) { + return directive_value.equals_ignoring_ascii_case(Directives::KeywordSources::UnsafeEval) + || directive_value.equals_ignoring_ascii_case(Directives::KeywordSources::WasmUnsafeEval); + }); + + if (maybe_unsafe_eval.is_end()) { + // 1. Let violation be the result of executing § 2.4.1 Create a violation object for global, policy, + // and directive on global, policy, and "script-src". + auto script_src_string = Directives::Names::ScriptSrc.to_string(); + auto violation = Violation::create_a_violation_object_for_global_policy_and_directive(realm, global, policy, script_src_string); + + // 2. Set violation’s resource to "wasm-eval". + violation->set_resource(Violation::Resource::WasmEval); + + // 3. Execute § 5.5 Report a violation on violation. + violation->report_a_violation(realm); + + // 4. If policy’s disposition is "enforce", then set result to "Blocked". + if (policy->disposition() == Policy::Disposition::Enforce) + result = Directives::Directive::Result::Blocked; + } + } + } + + // 4. If result is "Blocked", throw a WebAssembly.CompileError exception. + if (result == Directives::Directive::Result::Blocked) { + return realm.vm().throw_completion("Blocked by Content Security Policy"sv); + } + + return {}; +} + } diff --git a/Libraries/LibWeb/ContentSecurityPolicy/BlockingAlgorithms.h b/Libraries/LibWeb/ContentSecurityPolicy/BlockingAlgorithms.h index fc6b0c054fc..47ad7e0b781 100644 --- a/Libraries/LibWeb/ContentSecurityPolicy/BlockingAlgorithms.h +++ b/Libraries/LibWeb/ContentSecurityPolicy/BlockingAlgorithms.h @@ -6,6 +6,7 @@ #pragma once +#include #include namespace Web::ContentSecurityPolicy { @@ -22,4 +23,8 @@ Directives::Directive::Result should_navigation_response_to_navigation_request_o Directives::Directive::NavigationType navigation_type, GC::Ref target); +Directives::Directive::Result should_elements_inline_type_behavior_be_blocked_by_content_security_policy(JS::Realm&, GC::Ref element, Directives::Directive::InlineType type, String const& source); +JS::ThrowCompletionOr ensure_csp_does_not_block_string_compilation(JS::Realm& realm, ReadonlySpan parameter_strings, StringView body_string, StringView code_string, JS::CompilationType compilation_type, ReadonlySpan parameter_args, JS::Value body_arg); +JS::ThrowCompletionOr ensure_csp_does_not_block_wasm_byte_compilation(JS::Realm&); + } diff --git a/Libraries/LibWeb/ContentSecurityPolicy/Directives/DirectiveFactory.cpp b/Libraries/LibWeb/ContentSecurityPolicy/Directives/DirectiveFactory.cpp index 9c344ff5255..14f8fa69cdd 100644 --- a/Libraries/LibWeb/ContentSecurityPolicy/Directives/DirectiveFactory.cpp +++ b/Libraries/LibWeb/ContentSecurityPolicy/Directives/DirectiveFactory.cpp @@ -15,6 +15,7 @@ #include #include #include +#include namespace Web::ContentSecurityPolicy::Directives { @@ -41,6 +42,9 @@ GC::Ref create_directive(GC::Heap& heap, String name, Vector if (name == Names::ObjectSrc) return heap.allocate(move(name), move(value)); + if (name == Names::ScriptSrc) + return heap.allocate(move(name), move(value)); + return heap.allocate(move(name), move(value)); } diff --git a/Libraries/LibWeb/ContentSecurityPolicy/Directives/DirectiveOperations.cpp b/Libraries/LibWeb/ContentSecurityPolicy/Directives/DirectiveOperations.cpp index 244ba188c8b..2e3ca49a166 100644 --- a/Libraries/LibWeb/ContentSecurityPolicy/Directives/DirectiveOperations.cpp +++ b/Libraries/LibWeb/ContentSecurityPolicy/Directives/DirectiveOperations.cpp @@ -4,18 +4,26 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include #include +#include #include #include #include #include +#include +#include +#include #include #include #include #include +#include #include +#include +#include namespace Web::ContentSecurityPolicy::Directives { @@ -604,4 +612,401 @@ MatchResult does_response_match_source_list(GC::Refurl().value(), source_list, policy->self_origin(), request->redirect_count()); } +// https://w3c.github.io/webappsec-csp/#match-nonce-to-source-list +MatchResult does_nonce_match_source_list(String const& nonce, Vector const& source_list) +{ + // 1. Assert: source list is not null. + // Already done by only accept references. + + // 2. If nonce is the empty string, return "Does Not Match". + if (nonce.is_empty()) + return MatchResult::DoesNotMatch; + + // 3. For each expression of source list: + for (auto const& expression : source_list) { + // 1. If expression matches the nonce-source grammar, and nonce is identical to expression’s base64-value part, + // return "Matches". + auto nonce_source_match_result = parse_source_expression(Production::NonceSource, expression); + if (nonce_source_match_result.has_value()) { + VERIFY(nonce_source_match_result->base64_value.has_value()); + if (nonce == nonce_source_match_result->base64_value.value()) + return MatchResult::Matches; + } + } + + // 4. Return "Does Not Match". + return MatchResult::DoesNotMatch; +} + +// https://w3c.github.io/webappsec-csp/#match-integrity-metadata-to-source-list +// Spec Note: Here, we verify only whether the integrity metadata is a non-empty subset of the hash-source sources in +// source list. We rely on the browser’s enforcement of Subresource Integrity [SRI] to block non-matching +// resources upon response. +static MatchResult does_integrity_metadata_match_source_list(String const& integrity_metadata, Vector const& source_list) +{ + // 1. Assert: source list is not null. + // NOTE: This is already done by passing in source_list by reference. + + // 2. Let integrity expressions be the set of source expressions in source list that match the hash-source grammar. + Vector integrity_expressions; + for (auto const& expression : source_list) { + auto hash_source_parse_result = parse_source_expression(Production::HashSource, expression); + if (hash_source_parse_result.has_value()) + integrity_expressions.append(hash_source_parse_result.release_value()); + } + + // 3. If integrity expressions is empty, return "Does Not Match". + if (integrity_expressions.is_empty()) + return MatchResult::DoesNotMatch; + + // 4. Let integrity sources be the result of executing the algorithm defined in SRI § 3.3.3 Parse metadata. on + // integrity metadata. [SRI] + auto integrity_sources = MUST(SRI::parse_metadata(integrity_metadata)); + + // 5. If integrity sources is "no metadata" or an empty set, return "Does Not Match". + // FIXME: File a spec issue stating that this is targetting an older version of the SRI spec, which does not return + // "no metadata", but instead simply just returns an empty list if there is no metadata. + // The up-to-date spec is located at https://w3c.github.io/webappsec-subresource-integrity/ + if (integrity_sources.is_empty()) + return MatchResult::DoesNotMatch; + + // 6. For each source of integrity sources: + for (auto const& source : integrity_sources) { + // 1. If integrity expressions does not contain a source expression whose hash-algorithm is an ASCII + // case-insensitive match for source’s hash-algorithm, and whose base64-value is identical to source’s + // base64-value, return "Does Not Match". + auto maybe_match = integrity_expressions.find_if([&source](auto const& integrity_expression) { + VERIFY(integrity_expression.hash_algorithm.has_value()); + VERIFY(integrity_expression.base64_value.has_value()); + + return integrity_expression.hash_algorithm.value().equals_ignoring_ascii_case(source.algorithm) + && integrity_expression.base64_value.value() == source.base64_value; + }); + + if (maybe_match.is_end()) + return MatchResult::DoesNotMatch; + } + + // 7. Return "Matches". + return MatchResult::Matches; +} + +// https://w3c.github.io/webappsec-csp/#script-pre-request +Directive::Result script_directives_pre_request_check(GC::Ref request, GC::Ref directive, GC::Ref policy) +{ + // 1. If request’s destination is script-like: + if (request->destination_is_script_like()) { + // 1. If the result of executing § 6.7.2.3 Does nonce match source list? on request’s cryptographic nonce + // metadata and this directive’s value is "Matches", return "Allowed". + if (does_nonce_match_source_list(request->cryptographic_nonce_metadata(), directive->value()) == MatchResult::Matches) + return Directive::Result::Allowed; + + // 2. If the result of executing § 6.7.2.4 Does integrity metadata match source list? on request’s integrity + // metadata and this directive’s value is "Matches", return "Allowed". + if (does_integrity_metadata_match_source_list(request->integrity_metadata(), directive->value()) == MatchResult::Matches) + return Directive::Result::Allowed; + + // 3. If directive’s value contains a source expression that is an ASCII case-insensitive match for the + // "'strict-dynamic'" keyword-source: + // Spec Note: "'strict-dynamic'" is explained in more detail in § 8.2 Usage of "'strict-dynamic'". + // https://w3c.github.io/webappsec-csp/#strict-dynamic-usage + auto maybe_strict_dynamic = directive->value().find_if([](auto const& directive_value) { + return directive_value.equals_ignoring_ascii_case(KeywordSources::StrictDynamic); + }); + + if (!maybe_strict_dynamic.is_end()) { + // 1. If the request’s parser metadata is "parser-inserted", return "Blocked". + // Otherwise, return "Allowed". + if (request->parser_metadata() == Fetch::Infrastructure::Request::ParserMetadata::ParserInserted) + return Directive::Result::Blocked; + + return Directive::Result::Allowed; + } + + // 4. If the result of executing § 6.7.2.5 Does request match source list? on request, directive’s value, and + // policy, is "Does Not Match", return "Blocked". + if (does_request_match_source_list(request, directive->value(), policy) == MatchResult::DoesNotMatch) + return Directive::Result::Blocked; + } + + // 2. Return "Allowed". + return Directive::Result::Allowed; +} + +// https://w3c.github.io/webappsec-csp/#script-post-request +Directive::Result script_directives_post_request_check(GC::Ref request, GC::Ref response, GC::Ref directive, GC::Ref policy) +{ + // 1. If request’s destination is script-like: + if (request->destination_is_script_like()) { + // 1. If the result of executing § 6.7.2.3 Does nonce match source list? on request’s cryptographic nonce + // metadata and this directive’s value is "Matches", return "Allowed". + if (does_nonce_match_source_list(request->cryptographic_nonce_metadata(), directive->value()) == MatchResult::Matches) + return Directive::Result::Allowed; + + // 2. If the result of executing § 6.7.2.4 Does integrity metadata match source list? on request’s integrity + // metadata and this directive’s value is "Matches", return "Allowed". + if (does_integrity_metadata_match_source_list(request->integrity_metadata(), directive->value()) == MatchResult::Matches) + return Directive::Result::Allowed; + + // 3. If directive’s value contains "'strict-dynamic'": + // FIXME: Should this be case insensitive? + auto maybe_strict_dynamic = directive->value().find_if([](auto const& directive_value) { + return directive_value.equals_ignoring_ascii_case(KeywordSources::StrictDynamic); + }); + + if (!maybe_strict_dynamic.is_end()) { + // 1. If request’s parser metadata is not "parser-inserted", return "Allowed". + // Otherwise, return "Blocked". + if (request->parser_metadata() != Fetch::Infrastructure::Request::ParserMetadata::ParserInserted) + return Directive::Result::Allowed; + + return Directive::Result::Blocked; + } + + // 4. If the result of executing § 6.7.2.6 Does response to request match source list? on response, request, + // directive’s value, and policy, is "Does Not Match", return "Blocked". + if (does_response_match_source_list(response, request, directive->value(), policy) == MatchResult::DoesNotMatch) + return Directive::Result::Blocked; + } + + // 2. Return "Allowed". + return Directive::Result::Allowed; +} + +enum class [[nodiscard]] AllowsResult { + DoesNotAllow, + Allows, +}; + +static AllowsResult does_a_source_list_allow_all_inline_behavior_for_type(Vector const& source_list, Directive::InlineType type) +{ + // 1. Let allow all inline be false. + bool allow_all_inline = false; + + // 2. For each expression of list: + for (auto const& expression : source_list) { + // 1. If expression matches the nonce-source or hash-source grammar, return "Does Not Allow". + auto nonce_source_parse_result = parse_source_expression(Production::NonceSource, expression); + if (nonce_source_parse_result.has_value()) + return AllowsResult::DoesNotAllow; + + auto hash_source_parse_result = parse_source_expression(Production::HashSource, expression); + if (hash_source_parse_result.has_value()) + return AllowsResult::DoesNotAllow; + + // 2. If type is "script", "script attribute" or "navigation" and expression matches the keyword-source + // "'strict-dynamic'", return "Does Not Allow". + if (type == Directive::InlineType::Script || type == Directive::InlineType::ScriptAttribute || type == Directive::InlineType::Navigation) { + if (expression.equals_ignoring_ascii_case(KeywordSources::StrictDynamic)) + return AllowsResult::DoesNotAllow; + } + + // 3. If expression is an ASCII case-insensitive match for the keyword-source "'unsafe-inline'", set allow all + // inline to true. + if (expression.equals_ignoring_ascii_case(KeywordSources::UnsafeInline)) + allow_all_inline = true; + } + + // 3. If allow all inline is true, return "Allows". Otherwise, return "Does Not Allow". + return allow_all_inline ? AllowsResult::Allows : AllowsResult::DoesNotAllow; +} + +enum class NonceableResult { + NotNonceable, + Nonceable, +}; + +// https://w3c.github.io/webappsec-csp/#is-element-nonceable +[[nodiscard]] static NonceableResult is_element_nonceable(GC::Ptr element) +{ + // SPEC ISSUE 7: This processing is meant to mitigate the risk of dangling markup attacks that steal the nonce from + // an existing element in order to load injected script. It is fairly expensive, however, as it + // requires that we walk through all attributes and their values in order to determine whether the + // script should execute. Here, we try to minimize the impact by doing this check only for script + // elements when a nonce is present, but we should probably consider this algorithm as "at risk" + // until we know its impact. [Issue #w3c/webappsec-csp#98] (https://github.com/w3c/webappsec-csp/issues/98) + + // FIXME: See FIXME in `does_element_match_source_list_for_type_and_source` + if (!element) + return NonceableResult::NotNonceable; + + // 1. If element does not have an attribute named "nonce", return "Not Nonceable". + if (!is(element.ptr()) && !is(element.ptr())) + return NonceableResult::NotNonceable; + + if (!element->has_attribute(HTML::AttributeNames::nonce)) + return NonceableResult::NotNonceable; + + // 2. If element is a script element, then for each attribute of element’s attribute list: + // FIXME: File spec issue to ask if this should include SVGScriptElement. + if (is(element.ptr())) { + for (size_t attribute_index = 0; attribute_index < element->attributes()->length(); ++attribute_index) { + auto const* attribute = element->attributes()->item(attribute_index); + VERIFY(attribute); + + // 1. If attribute’s name contains an ASCII case-insensitive match for "name().to_string(); + if (attribute_name.contains("value(); + if (attribute_value.contains("had_duplicate_attribute_during_tokenization()) + return NonceableResult::NotNonceable; + + // 4. Return "Nonceable". + return NonceableResult::Nonceable; +} + +// https://w3c.github.io/webappsec-csp/#match-element-to-source-list +MatchResult does_element_match_source_list_for_type_and_source(GC::Ptr element, Vector const& source_list, Directive::InlineType type, String const& source) +{ + // Spec Note: Regardless of the encoding of the document, source will be converted to UTF-8 before applying any + // hashing algorithms. + + // 1. If § 6.7.3.2 Does a source list allow all inline behavior for type? returns "Allows" given list and type, + // return "Matches". + if (does_a_source_list_allow_all_inline_behavior_for_type(source_list, type) == AllowsResult::Allows) + return MatchResult::Matches; + + // 2. If type is "script" or "style", and § 6.7.3.1 Is element nonceable? returns "Nonceable" when executed upon + // element: + // Spec Note: Nonces only apply to inline script and inline style, not to attributes of either element or to + // javascript: navigations. + // FIXME: File spec issue that this algorithm doesn't handle `element` being null, which is it when doing a + // javascript: URL navigation. For now, we say that the element is not nonceable if it's null, because + // we simply can't pull a nonce attribute value from a null element. + if ((type == Directive::InlineType::Script || type == Directive::InlineType::Style) && is_element_nonceable(element) == NonceableResult::Nonceable) { + // 1. For each expression of list: + for (auto const& expression : source_list) { + // 1. If expression matches the nonce-source grammar, and element has a nonce attribute whose value is + // expression's base64-value part, return "Matches". + auto nonce_source_parse_result = parse_source_expression(Production::NonceSource, expression); + if (nonce_source_parse_result.has_value()) { + VERIFY(element); + VERIFY(is(element.ptr()) || is(element.ptr())); + + String element_nonce; + if (is(element.ptr())) { + auto const& html_element = static_cast(*element); + element_nonce = html_element.nonce(); + } else { + auto const& svg_element = as(*element); + element_nonce = svg_element.nonce(); + } + + if (nonce_source_parse_result->base64_value == element_nonce) + return MatchResult::Matches; + } + } + } + + // 3. Let unsafe-hashes flag be false. + bool unsafe_hashes_flag = false; + + // 4. For each expression of list: + for (auto const& expression : source_list) { + // 1. If expression is an ASCII case-insensitive match for the keyword-source "'unsafe-hashes'", set + // unsafe-hashes flag to true. Break out of the loop. + if (expression.equals_ignoring_ascii_case(KeywordSources::UnsafeHashes)) { + unsafe_hashes_flag = true; + break; + } + } + + // 5. If type is "script" or "style", or unsafe-hashes flag is true: + // NOTE: Hashes apply to inline script and inline style. If the "'unsafe-hashes'" source expression is present, + // they will also apply to event handlers, style attributes and javascript: navigations. + // SPEC ISSUE 8: This should handle 'strict-dynamic' for dynamically inserted inline scripts. + // [Issue #w3c/webappsec-csp#426] (https://github.com/w3c/webappsec-csp/issues/426) + if (type == Directive::InlineType::Script || type == Directive::InlineType::Style || unsafe_hashes_flag) { + // 1. Set source to the result of executing UTF-8 encode on the result of executing JavaScript string + // converting on source. + auto converted_source = MUST(Infra::convert_to_scalar_value_string(source)); + + // NOTE: converted_source is already UTF-8 encoded. + auto converted_source_bytes = converted_source.bytes(); + + // 2. For each expression of list: + for (auto const& expression : source_list) { + // 1. If expression matches the hash-source grammar: + auto hash_source_parse_result = parse_source_expression(Production::HashSource, expression); + if (hash_source_parse_result.has_value()) { + // 1. Let algorithm be null. + StringView algorithm; + + // 2. If expression’s hash-algorithm part is an ASCII case-insensitive match for "sha256", set + // algorithm to SHA-256. + VERIFY(hash_source_parse_result->hash_algorithm.has_value()); + auto hash_algorithm_from_expression = hash_source_parse_result->hash_algorithm.value(); + + if (hash_algorithm_from_expression.equals_ignoring_ascii_case("sha256"sv)) + algorithm = "SHA-256"sv; + + // 3. If expression’s hash-algorithm part is an ASCII case-insensitive match for "sha384", set + // algorithm to SHA-384. + if (hash_algorithm_from_expression.equals_ignoring_ascii_case("sha384"sv)) + algorithm = "SHA-384"sv; + + // 4. If expression’s hash-algorithm part is an ASCII case-insensitive match for "sha512", set + // algorithm to SHA-512. + if (hash_algorithm_from_expression.equals_ignoring_ascii_case("sha512"sv)) + algorithm = "SHA-512"sv; + + // 5. If algorithm is not null: + if (!algorithm.is_null()) { + // 1. Let actual be the result of base64 encoding the result of applying algorithm to source. + auto apply_algorithm_to_source = [&] { + if (algorithm == "SHA-256"sv) { + auto result = ::Crypto::Hash::SHA256::hash(converted_source_bytes); + return MUST(encode_base64(result.bytes())); + } + + if (algorithm == "SHA-384"sv) { + auto result = ::Crypto::Hash::SHA384::hash(converted_source_bytes); + return MUST(encode_base64(result.bytes())); + } + + if (algorithm == "SHA-512"sv) { + auto result = ::Crypto::Hash::SHA512::hash(converted_source_bytes); + return MUST(encode_base64(result.bytes())); + } + + VERIFY_NOT_REACHED(); + }; + + auto actual = apply_algorithm_to_source(); + + // 2. Let expected be expression’s base64-value part, with all '-' characters replaced with '+', + // and all '_' characters replaced with '/'. + // Spec Note: This replacement normalizes hashes expressed in base64url encoding into base64 + // encoding for matching. + VERIFY(hash_source_parse_result->base64_value.has_value()); + auto base64_value_string = MUST(String::from_utf8(hash_source_parse_result->base64_value.value())); + + auto expected = MUST(base64_value_string.replace("-"sv, "+"sv, ReplaceMode::All)); + expected = MUST(expected.replace("_"sv, "/"sv, ReplaceMode::All)); + + // 3. If actual is identical to expected, return "Matches". + if (actual == expected) + return MatchResult::Matches; + } + } + } + } + + // 6. Return "Does Not Match". + return MatchResult::DoesNotMatch; +} + } diff --git a/Libraries/LibWeb/ContentSecurityPolicy/Directives/DirectiveOperations.h b/Libraries/LibWeb/ContentSecurityPolicy/Directives/DirectiveOperations.h index 4e862019d84..9d547a46fff 100644 --- a/Libraries/LibWeb/ContentSecurityPolicy/Directives/DirectiveOperations.h +++ b/Libraries/LibWeb/ContentSecurityPolicy/Directives/DirectiveOperations.h @@ -35,5 +35,11 @@ MatchResult does_url_match_source_list_in_origin_with_redirect_count(URL::URL co MatchResult does_request_match_source_list(GC::Ref request, Vector const& source_list, GC::Ref policy); MatchResult does_response_match_source_list(GC::Ref response, GC::Ref request, Vector const& source_list, GC::Ref policy); +MatchResult does_nonce_match_source_list(String const& nonce, Vector const& source_list); + +Directive::Result script_directives_pre_request_check(GC::Ref request, GC::Ref directive, GC::Ref policy); +Directive::Result script_directives_post_request_check(GC::Ref request, GC::Ref response, GC::Ref directive, GC::Ref policy); + +MatchResult does_element_match_source_list_for_type_and_source(GC::Ptr element, Vector const& source_list, Directive::InlineType type, String const& source); } diff --git a/Libraries/LibWeb/ContentSecurityPolicy/Directives/ScriptSourceDirective.cpp b/Libraries/LibWeb/ContentSecurityPolicy/Directives/ScriptSourceDirective.cpp new file mode 100644 index 00000000000..d97ab7dae52 --- /dev/null +++ b/Libraries/LibWeb/ContentSecurityPolicy/Directives/ScriptSourceDirective.cpp @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2024, Luke Wilde + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include + +namespace Web::ContentSecurityPolicy::Directives { + +GC_DEFINE_ALLOCATOR(ScriptSourceDirective); + +ScriptSourceDirective::ScriptSourceDirective(String name, Vector value) + : Directive(move(name), move(value)) +{ +} + +// https://w3c.github.io/webappsec-csp/#script-src-pre-request +Directive::Result ScriptSourceDirective::pre_request_check(GC::Heap&, GC::Ref request, GC::Ref policy) const +{ + // 1. Let name be the result of executing § 6.8.1 Get the effective directive for request on request. + auto name = get_the_effective_directive_for_request(request); + + // 2. If the result of executing § 6.8.4 Should fetch directive execute on name, script-src and policy is "No", + // return "Allowed". + if (should_fetch_directive_execute(name, Names::ScriptSrc, policy) == ShouldExecute::No) + return Result::Allowed; + + // 3. Return the result of executing § 6.7.1.1 Script directives pre-request check on request, this directive, + // and policy. + return script_directives_pre_request_check(request, *this, policy); +} + +// https://w3c.github.io/webappsec-csp/#script-src-post-request +Directive::Result ScriptSourceDirective::post_request_check(GC::Heap&, GC::Ref request, GC::Ref response, GC::Ref policy) const +{ + // 1. Let name be the result of executing § 6.8.1 Get the effective directive for request on request. + auto name = get_the_effective_directive_for_request(request); + + // 2. If the result of executing § 6.8.4 Should fetch directive execute on name, script-src and policy is "No", + // return "Allowed". + if (should_fetch_directive_execute(name, Names::ScriptSrc, policy) == ShouldExecute::No) + return Result::Allowed; + + // 3. Return the result of executing § 6.7.1.2 Script directives post-request check on request, response, this + // directive, and policy. + return script_directives_post_request_check(request, response, *this, policy); +} + +// https://w3c.github.io/webappsec-csp/#script-src-inline +Directive::Result ScriptSourceDirective::inline_check(GC::Heap&, GC::Ptr element, InlineType type, GC::Ref policy, String const& source) const +{ + // 1. Assert: element is not null or type is "navigation". + VERIFY(element || type == InlineType::Navigation); + + // 2. Let name be the result of executing § 6.8.2 Get the effective directive for inline checks on type. + auto name = get_the_effective_directive_for_inline_checks(type); + + // 3. If the result of executing § 6.8.4 Should fetch directive execute on name, script-src and policy is "No", + // return "Allowed". + if (should_fetch_directive_execute(name, Names::ScriptSrc, policy) == ShouldExecute::No) + return Result::Allowed; + + // 4. If the result of executing § 6.7.3.3 Does element match source list for type and source? on element, this + // directive’s value, type, and source, is "Does Not Match", return "Blocked". + if (does_element_match_source_list_for_type_and_source(element, value(), type, source) == MatchResult::DoesNotMatch) + return Result::Blocked; + + // 5. Return "Allowed". + return Result::Allowed; +} + +} diff --git a/Libraries/LibWeb/ContentSecurityPolicy/Directives/ScriptSourceDirective.h b/Libraries/LibWeb/ContentSecurityPolicy/Directives/ScriptSourceDirective.h new file mode 100644 index 00000000000..847057bed6f --- /dev/null +++ b/Libraries/LibWeb/ContentSecurityPolicy/Directives/ScriptSourceDirective.h @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2024, Luke Wilde + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace Web::ContentSecurityPolicy::Directives { + +// https://w3c.github.io/webappsec-csp/#directive-script-src +class ScriptSourceDirective final : public Directive { + GC_CELL(ScriptSourceDirective, Directive) + GC_DECLARE_ALLOCATOR(ScriptSourceDirective); + +public: + virtual ~ScriptSourceDirective() = default; + + virtual Result pre_request_check(GC::Heap&, GC::Ref, GC::Ref) const override; + virtual Result post_request_check(GC::Heap&, GC::Ref, GC::Ref, GC::Ref) const override; + virtual Result inline_check(GC::Heap&, GC::Ptr, InlineType, GC::Ref, String const&) const override; + +private: + ScriptSourceDirective(String name, Vector value); +}; + +} diff --git a/Libraries/LibWeb/DOM/EventTarget.cpp b/Libraries/LibWeb/DOM/EventTarget.cpp index 223f25d00cd..a71486c2ac0 100644 --- a/Libraries/LibWeb/DOM/EventTarget.cpp +++ b/Libraries/LibWeb/DOM/EventTarget.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -776,7 +777,12 @@ void EventTarget::element_event_handler_attribute_changed(FlyString const& local } // 5. Otherwise: - // FIXME: 1. If the Should element's inline behavior be blocked by Content Security Policy? algorithm returns "Blocked" when executed upon element, "script attribute", and value, then return. [CSP] + // 1. If the Should element's inline behavior be blocked by Content Security Policy? algorithm returns "Blocked" when executed upon element, "script attribute", and value, then return. [CSP] + auto& this_as_element = as(*this); + if (ContentSecurityPolicy::should_elements_inline_type_behavior_be_blocked_by_content_security_policy(realm(), this_as_element, ContentSecurityPolicy::Directives::Directive::InlineType::ScriptAttribute, value.value()) == ContentSecurityPolicy::Directives::Directive::Result::Blocked) { + dbgln("EventTarget: Refusing to add inline event handler as it violates the Content Security Policy."); + return; + } // 2. Let handlerMap be eventTarget's event handler map. auto& handler_map = event_target->ensure_data().event_handler_map; diff --git a/Libraries/LibWeb/Forward.h b/Libraries/LibWeb/Forward.h index 2db3a52be92..8d7f7f1264f 100644 --- a/Libraries/LibWeb/Forward.h +++ b/Libraries/LibWeb/Forward.h @@ -138,6 +138,7 @@ class ImageSourceDirective; class ManifestSourceDirective; class MediaSourceDirective; class ObjectSourceDirective; +class ScriptSourceDirective; struct SerializedDirective; } diff --git a/Libraries/LibWeb/HTML/HTMLOrSVGElement.h b/Libraries/LibWeb/HTML/HTMLOrSVGElement.h index 937947c6382..15323590d3b 100644 --- a/Libraries/LibWeb/HTML/HTMLOrSVGElement.h +++ b/Libraries/LibWeb/HTML/HTMLOrSVGElement.h @@ -18,7 +18,7 @@ public: [[nodiscard]] GC::Ref dataset(); // https://html.spec.whatwg.org/multipage/urls-and-fetching.html#dom-noncedelement-nonce - String const& nonce() { return m_cryptographic_nonce; } + String const& nonce() const { return m_cryptographic_nonce; } void set_nonce(String const& nonce) { m_cryptographic_nonce = nonce; } void focus(); diff --git a/Libraries/LibWeb/HTML/HTMLScriptElement.cpp b/Libraries/LibWeb/HTML/HTMLScriptElement.cpp index 1dca128d53a..c9c37aadac3 100644 --- a/Libraries/LibWeb/HTML/HTMLScriptElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLScriptElement.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -274,8 +275,13 @@ void HTMLScriptElement::prepare_script() return; } - // FIXME: 19. If el does not have a src content attribute, and the Should element's inline behavior be blocked by Content Security Policy? - // algorithm returns "Blocked" when given el, "script", and source text, then return. [CSP] + // 19. If el does not have a src content attribute, and the Should element's inline behavior be blocked by Content Security Policy? + // algorithm returns "Blocked" when given el, "script", and source text, then return. [CSP] + if (!has_attribute(AttributeNames::src) + && ContentSecurityPolicy::should_elements_inline_type_behavior_be_blocked_by_content_security_policy(realm(), *this, ContentSecurityPolicy::Directives::Directive::InlineType::Script, source_text) == ContentSecurityPolicy::Directives::Directive::Result::Blocked) { + dbgln("HTMLScriptElement: Refusing to run inline script because it violates the Content Security Policy."); + return; + } // 20. If el has an event attribute and a for attribute, and el's type is "classic", then: if (m_script_type == ScriptType::Classic && has_attribute(HTML::AttributeNames::event) && has_attribute(HTML::AttributeNames::for_)) { @@ -325,7 +331,8 @@ void HTMLScriptElement::prepare_script() // 23. Let module script credentials mode be the CORS settings attribute credentials mode for el's crossorigin content attribute. auto module_script_credential_mode = cors_settings_attribute_credentials_mode(m_crossorigin); - // FIXME: 24. Let cryptographic nonce be el's [[CryptographicNonce]] internal slot's value. + // 24. Let cryptographic nonce be el's [[CryptographicNonce]] internal slot's value. + auto cryptographic_nonce = m_cryptographic_nonce; // 25. If el has an integrity attribute, then let integrity metadata be that attribute's value. // Otherwise, let integrity metadata be the empty string. @@ -350,7 +357,7 @@ void HTMLScriptElement::prepare_script() // credentials mode is module script credentials mode, referrer policy is referrer policy, // and fetch priority is fetch priority. ScriptFetchOptions options { - .cryptographic_nonce = {}, // FIXME + .cryptographic_nonce = move(cryptographic_nonce), .integrity_metadata = move(integrity_metadata), .parser_metadata = parser_metadata, .credentials_mode = module_script_credential_mode, diff --git a/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.cpp b/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.cpp index 6e6d93bbf54..3e9d6293a1e 100644 --- a/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.cpp +++ b/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -342,7 +343,9 @@ i32 WindowOrWorkerGlobalScopeMixin::run_timer_initialization_steps(TimerHandler timeout = 0; // FIXME: 5. If nesting level is greater than 5, and timeout is less than 4, then set timeout to 4. - // FIXME: 6. Let realm be global's relevant realm. + + // 6. Let realm be global's relevant realm. + auto& realm = relevant_realm(this_impl()); // 7. Let initiating script be the active script. auto const* initiating_script = Web::Bindings::active_script(); @@ -352,7 +355,7 @@ i32 WindowOrWorkerGlobalScopeMixin::run_timer_initialization_steps(TimerHandler // FIXME 8. Let uniqueHandle be null. // 9. Let task be a task that runs the following substeps: - auto task = GC::create_function(vm.heap(), Function([this, handler = move(handler), timeout, arguments = move(arguments), repeat, id, initiating_script, previous_id]() { + auto task = GC::create_function(vm.heap(), Function([this, handler = move(handler), timeout, arguments = move(arguments), repeat, id, initiating_script, previous_id, &vm, &realm]() { // FIXME: 1. Assert: uniqueHandle is a unique internal value, not null. // 2. If id does not exist in global's map of setTimeout and setInterval IDs, then abort these steps. @@ -362,10 +365,11 @@ i32 WindowOrWorkerGlobalScopeMixin::run_timer_initialization_steps(TimerHandler // FIXME: 3. If global's map of setTimeout and setInterval IDs[id] does not equal uniqueHandle, then abort these steps. // FIXME: 4. Record timing info for timer handler given handler, global's relevant settings object, and repeat. - handler.visit( + bool continue_ = handler.visit( // 5. If handler is a Function, then invoke handler given arguments and "report", and with callback this value set to thisArg. [&](GC::Root const& callback) { (void)WebIDL::invoke_callback(*callback, &this_impl(), WebIDL::ExceptionBehavior::Report, arguments); + return true; }, // 6. Otherwise: [&](String const& source) { @@ -383,8 +387,14 @@ i32 WindowOrWorkerGlobalScopeMixin::run_timer_initialization_steps(TimerHandler // FIXME: 4. Set handler to the result of invoking the Get Trusted Type compliant string algorithm with TrustedScript, global, handler, sink, and "script". } - // FIXME: 2. Assert: handler is a string. - // FIXME: 3. Perform EnsureCSPDoesNotBlockStringCompilation(realm, « », handler, handler, timer, « », handler). If this throws an exception, catch it, report it for global, and abort these steps. + // 2. Assert: handler is a string. + // 3. Perform EnsureCSPDoesNotBlockStringCompilation(realm, « », handler, handler, timer, « », handler). + // If this throws an exception, catch it, report it for global, and abort these steps. + auto handler_primitive_string = JS::PrimitiveString::create(vm, source); + if (auto result = ContentSecurityPolicy::ensure_csp_does_not_block_string_compilation(realm, {}, source, source, JS::CompilationType::Timer, {}, handler_primitive_string); result.is_throw_completion()) { + report_exception(result, realm); + return false; + } // 4. Let settings object be global's relevant settings object. auto& settings_object = relevant_settings_object(this_impl()); @@ -415,8 +425,12 @@ i32 WindowOrWorkerGlobalScopeMixin::run_timer_initialization_steps(TimerHandler // 9. Run the classic script script. (void)script->run(); + return true; }); + if (!continue_) + return; + // 7. If id does not exist in global's map of setTimeout and setInterval IDs, then abort these steps. if (!m_timers.contains(id)) return; diff --git a/Libraries/LibWeb/WebAssembly/WebAssembly.cpp b/Libraries/LibWeb/WebAssembly/WebAssembly.cpp index 57c94f4fac7..35bcc532497 100644 --- a/Libraries/LibWeb/WebAssembly/WebAssembly.cpp +++ b/Libraries/LibWeb/WebAssembly/WebAssembly.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -149,6 +150,7 @@ WebIDL::ExceptionOr> compile_streaming(JS::VM& vm, GC:: } // https://webassembly.github.io/spec/js-api/#dom-webassembly-instantiate +// https://webassembly.github.io/content-security-policy/js-api/#dom-webassembly-instantiate WebIDL::ExceptionOr> instantiate(JS::VM& vm, GC::Root& bytes, Optional>& import_object_handle) { auto& realm = *vm.current_realm(); @@ -160,10 +162,13 @@ WebIDL::ExceptionOr> instantiate(JS::VM& vm, GC::Root(vm.error_message(JS::VM::ErrorMessage::OutOfMemory))); } - // 2. Asynchronously compile a WebAssembly module from stableBytes and let promiseOfModule be the result. + // 2. Perform HostEnsureCanCompileWasmBytes() + TRY(Detail::host_ensure_can_compile_wasm_bytes(vm)); + + // 3. Asynchronously compile a WebAssembly module from stableBytes and let promiseOfModule be the result. auto promise_of_module = asynchronously_compile_webassembly_module(vm, stable_bytes.release_value()); - // 3. Instantiate promiseOfModule with imports importObject and return the result. + // 4. Instantiate promiseOfModule with imports importObject and return the result. GC::Ptr const import_object = import_object_handle.has_value() ? import_object_handle.value().ptr() : nullptr; return instantiate_promise_of_module(vm, promise_of_module, import_object); } @@ -410,8 +415,11 @@ JS::ThrowCompletionOr> instantiate_module(JS } // // https://webassembly.github.io/spec/js-api/#compile-a-webassembly-module +// https://webassembly.github.io/content-security-policy/js-api/#compile-a-webassembly-module JS::ThrowCompletionOr> compile_a_webassembly_module(JS::VM& vm, ByteBuffer data) { + TRY(host_ensure_can_compile_wasm_bytes(vm)); + FixedMemoryStream stream { data.bytes() }; auto module_result = Wasm::Module::parse(stream); if (module_result.is_error()) { @@ -626,6 +634,18 @@ JS::Value to_js_value(JS::VM& vm, Wasm::Value& wasm_value, Wasm::ValueType type) VERIFY_NOT_REACHED(); } +// https://webassembly.github.io/content-security-policy/js-api/#abstract-opdef-hostensurecancompilewasmbytes +JS::ThrowCompletionOr host_ensure_can_compile_wasm_bytes(JS::VM& vm) +{ + // 1. Let realm be the current Realm. + auto& realm = *vm.current_realm(); + + // 2. Perform EnsureCSPDoesNotBlockWasmByteCompilation(realm) + // This algorithm does not return a value, but raises a CompileError exception if the operation cannot complete + // successfully. + return ContentSecurityPolicy::ensure_csp_does_not_block_wasm_byte_compilation(realm); +} + } // https://webassembly.github.io/spec/js-api/#asynchronously-compile-a-webassembly-module diff --git a/Libraries/LibWeb/WebAssembly/WebAssembly.h b/Libraries/LibWeb/WebAssembly/WebAssembly.h index 7500569329c..c83aca2e79d 100644 --- a/Libraries/LibWeb/WebAssembly/WebAssembly.h +++ b/Libraries/LibWeb/WebAssembly/WebAssembly.h @@ -94,6 +94,7 @@ JS::NativeFunction* create_native_function(JS::VM&, Wasm::FunctionAddress addres JS::ThrowCompletionOr to_webassembly_value(JS::VM&, JS::Value value, Wasm::ValueType const& type); Wasm::Value default_webassembly_value(JS::VM&, Wasm::ValueType type); JS::Value to_js_value(JS::VM&, Wasm::Value& wasm_value, Wasm::ValueType type); +JS::ThrowCompletionOr host_ensure_can_compile_wasm_bytes(JS::VM&); extern HashMap, WebAssemblyCache> s_caches;