mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-07-29 04:09:13 +00:00
LibWeb/CSP: Implement the script-src directive
This commit is contained in:
parent
3d43462ccd
commit
0cff47828d
Notes:
github-actions[bot]
2025-07-09 21:53:59 +00:00
Author: https://github.com/Lubrsi
Commit: 0cff47828d
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/5328
Reviewed-by: https://github.com/ADKaster ✅
17 changed files with 839 additions and 14 deletions
|
@ -49,6 +49,7 @@ enum class CompilationType {
|
|||
DirectEval,
|
||||
IndirectEval,
|
||||
Function,
|
||||
Timer,
|
||||
};
|
||||
|
||||
class JS_API VM : public RefCounted<VM> {
|
||||
|
|
|
@ -25,6 +25,9 @@
|
|||
#include <LibWeb/Bindings/MainThreadVM.h>
|
||||
#include <LibWeb/Bindings/SyntheticHostDefined.h>
|
||||
#include <LibWeb/Bindings/WindowExposedInterfaces.h>
|
||||
#include <LibWeb/ContentSecurityPolicy/BlockingAlgorithms.h>
|
||||
#include <LibWeb/ContentSecurityPolicy/Directives/KeywordSources.h>
|
||||
#include <LibWeb/ContentSecurityPolicy/Directives/Names.h>
|
||||
#include <LibWeb/DOM/Document.h>
|
||||
#include <LibWeb/HTML/CustomElements/CustomElementDefinition.h>
|
||||
#include <LibWeb/HTML/EventNames.h>
|
||||
|
@ -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<String> parameter_strings, StringView body_string, StringView code_string, JS::CompilationType compilation_type, ReadonlySpan<JS::Value> parameter_args, JS::Value body_arg) -> JS::ThrowCompletionOr<void> {
|
||||
// 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<JS::PrimitiveString> {
|
||||
// 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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -6,11 +6,16 @@
|
|||
|
||||
#include <LibWeb/ContentSecurityPolicy/BlockingAlgorithms.h>
|
||||
#include <LibWeb/ContentSecurityPolicy/Directives/DirectiveOperations.h>
|
||||
#include <LibWeb/ContentSecurityPolicy/Directives/KeywordSources.h>
|
||||
#include <LibWeb/ContentSecurityPolicy/Directives/Names.h>
|
||||
#include <LibWeb/ContentSecurityPolicy/PolicyList.h>
|
||||
#include <LibWeb/ContentSecurityPolicy/Violation.h>
|
||||
#include <LibWeb/DOM/Document.h>
|
||||
#include <LibWeb/DOM/Element.h>
|
||||
#include <LibWeb/Fetch/Infrastructure/HTTP/Requests.h>
|
||||
#include <LibWeb/Fetch/Infrastructure/HTTP/Responses.h>
|
||||
#include <LibWeb/Infra/Strings.h>
|
||||
#include <LibWeb/WebAssembly/WebAssembly.h>
|
||||
|
||||
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<DOM::Element> 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<void> ensure_csp_does_not_block_string_compilation(JS::Realm& realm, ReadonlySpan<String>, StringView, StringView code_string, JS::CompilationType, ReadonlySpan<JS::Value>, 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<Vector<String>> 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<JS::EvalError>("Blocked by Content Security Policy"sv);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webappsec-csp/#can-compile-wasm-bytes
|
||||
JS::ThrowCompletionOr<void> 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<Vector<String>> 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<WebAssembly::CompileError>("Blocked by Content Security Policy"sv);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include <LibJS/Runtime/VM.h>
|
||||
#include <LibWeb/ContentSecurityPolicy/Directives/Directive.h>
|
||||
|
||||
namespace Web::ContentSecurityPolicy {
|
||||
|
@ -22,4 +23,8 @@ Directives::Directive::Result should_navigation_response_to_navigation_request_o
|
|||
Directives::Directive::NavigationType navigation_type,
|
||||
GC::Ref<HTML::Navigable> target);
|
||||
|
||||
Directives::Directive::Result should_elements_inline_type_behavior_be_blocked_by_content_security_policy(JS::Realm&, GC::Ref<DOM::Element> element, Directives::Directive::InlineType type, String const& source);
|
||||
JS::ThrowCompletionOr<void> ensure_csp_does_not_block_string_compilation(JS::Realm& realm, ReadonlySpan<String> parameter_strings, StringView body_string, StringView code_string, JS::CompilationType compilation_type, ReadonlySpan<JS::Value> parameter_args, JS::Value body_arg);
|
||||
JS::ThrowCompletionOr<void> ensure_csp_does_not_block_wasm_byte_compilation(JS::Realm&);
|
||||
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
#include <LibWeb/ContentSecurityPolicy/Directives/MediaSourceDirective.h>
|
||||
#include <LibWeb/ContentSecurityPolicy/Directives/Names.h>
|
||||
#include <LibWeb/ContentSecurityPolicy/Directives/ObjectSourceDirective.h>
|
||||
#include <LibWeb/ContentSecurityPolicy/Directives/ScriptSourceDirective.h>
|
||||
|
||||
namespace Web::ContentSecurityPolicy::Directives {
|
||||
|
||||
|
@ -41,6 +42,9 @@ GC::Ref<Directive> create_directive(GC::Heap& heap, String name, Vector<String>
|
|||
if (name == Names::ObjectSrc)
|
||||
return heap.allocate<ObjectSourceDirective>(move(name), move(value));
|
||||
|
||||
if (name == Names::ScriptSrc)
|
||||
return heap.allocate<ScriptSourceDirective>(move(name), move(value));
|
||||
|
||||
return heap.allocate<Directive>(move(name), move(value));
|
||||
}
|
||||
|
||||
|
|
|
@ -4,18 +4,26 @@
|
|||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/Base64.h>
|
||||
#include <AK/FlyString.h>
|
||||
#include <AK/HashMap.h>
|
||||
#include <AK/Vector.h>
|
||||
#include <LibCrypto/Hash/SHA2.h>
|
||||
#include <LibWeb/ContentSecurityPolicy/Directives/DirectiveOperations.h>
|
||||
#include <LibWeb/ContentSecurityPolicy/Directives/KeywordSources.h>
|
||||
#include <LibWeb/ContentSecurityPolicy/Directives/Names.h>
|
||||
#include <LibWeb/ContentSecurityPolicy/Directives/SourceExpression.h>
|
||||
#include <LibWeb/DOM/Attr.h>
|
||||
#include <LibWeb/DOM/Element.h>
|
||||
#include <LibWeb/DOM/NamedNodeMap.h>
|
||||
#include <LibWeb/DOMURL/DOMURL.h>
|
||||
#include <LibWeb/Fetch/Infrastructure/HTTP/Requests.h>
|
||||
#include <LibWeb/Fetch/Infrastructure/HTTP/Responses.h>
|
||||
#include <LibWeb/Fetch/Infrastructure/URL.h>
|
||||
#include <LibWeb/HTML/HTMLScriptElement.h>
|
||||
#include <LibWeb/Infra/Strings.h>
|
||||
#include <LibWeb/SRI/SRI.h>
|
||||
#include <LibWeb/SVG/SVGElement.h>
|
||||
|
||||
namespace Web::ContentSecurityPolicy::Directives {
|
||||
|
||||
|
@ -604,4 +612,401 @@ MatchResult does_response_match_source_list(GC::Ref<Fetch::Infrastructure::Respo
|
|||
return does_url_match_source_list_in_origin_with_redirect_count(response->url().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<String> 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<String> 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<SourceExpressionParseResult> 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<Fetch::Infrastructure::Request const> request, GC::Ref<Directive const> directive, GC::Ref<Policy const> 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<Fetch::Infrastructure::Request const> request, GC::Ref<Fetch::Infrastructure::Response const> response, GC::Ref<Directive const> directive, GC::Ref<Policy const> 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<String> 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<DOM::Element const> 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<HTML::HTMLElement>(element.ptr()) && !is<SVG::SVGElement>(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<HTML::HTMLScriptElement>(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 "<script" or "<style", return
|
||||
// "Not Nonceable".
|
||||
auto attribute_name = attribute->name().to_string();
|
||||
if (attribute_name.contains("<script"sv, CaseSensitivity::CaseInsensitive) || attribute_name.contains("<style"sv, CaseSensitivity::CaseInsensitive))
|
||||
return NonceableResult::NotNonceable;
|
||||
|
||||
// 2. If attribute’s value contains an ASCII case-insensitive match for "<script" or "<style", return
|
||||
// "Not Nonceable".
|
||||
auto const& attribute_value = attribute->value();
|
||||
if (attribute_value.contains("<script"sv, CaseSensitivity::CaseInsensitive) || attribute_value.contains("<style"sv, CaseSensitivity::CaseInsensitive))
|
||||
return NonceableResult::NotNonceable;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. If element had a duplicate-attribute parse error during tokenization, return "Not Nonceable".
|
||||
// SPEC ISSUE 6: We need some sort of hook in HTML to record this error if we’re planning on using it here.
|
||||
// [Issue #whatwg/html#3257] (https://github.com/whatwg/html/issues/3257)
|
||||
if (element->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<DOM::Element const> element, Vector<String> 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<HTML::HTMLElement>(element.ptr()) || is<SVG::SVGElement>(element.ptr()));
|
||||
|
||||
String element_nonce;
|
||||
if (is<HTML::HTMLElement>(element.ptr())) {
|
||||
auto const& html_element = static_cast<HTML::HTMLElement const&>(*element);
|
||||
element_nonce = html_element.nonce();
|
||||
} else {
|
||||
auto const& svg_element = as<SVG::SVGElement>(*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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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<Fetch::Infrastructure::Request const> request, Vector<String> const& source_list, GC::Ref<Policy const> policy);
|
||||
MatchResult does_response_match_source_list(GC::Ref<Fetch::Infrastructure::Response const> response, GC::Ref<Fetch::Infrastructure::Request const> request, Vector<String> const& source_list, GC::Ref<Policy const> policy);
|
||||
MatchResult does_nonce_match_source_list(String const& nonce, Vector<String> const& source_list);
|
||||
|
||||
Directive::Result script_directives_pre_request_check(GC::Ref<Fetch::Infrastructure::Request const> request, GC::Ref<Directive const> directive, GC::Ref<Policy const> policy);
|
||||
Directive::Result script_directives_post_request_check(GC::Ref<Fetch::Infrastructure::Request const> request, GC::Ref<Fetch::Infrastructure::Response const> response, GC::Ref<Directive const> directive, GC::Ref<Policy const> policy);
|
||||
|
||||
MatchResult does_element_match_source_list_for_type_and_source(GC::Ptr<DOM::Element const> element, Vector<String> const& source_list, Directive::InlineType type, String const& source);
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Copyright (c) 2024, Luke Wilde <luke@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibWeb/ContentSecurityPolicy/Directives/DirectiveOperations.h>
|
||||
#include <LibWeb/ContentSecurityPolicy/Directives/Names.h>
|
||||
#include <LibWeb/ContentSecurityPolicy/Directives/ScriptSourceDirective.h>
|
||||
#include <LibWeb/Fetch/Infrastructure/HTTP/Requests.h>
|
||||
|
||||
namespace Web::ContentSecurityPolicy::Directives {
|
||||
|
||||
GC_DEFINE_ALLOCATOR(ScriptSourceDirective);
|
||||
|
||||
ScriptSourceDirective::ScriptSourceDirective(String name, Vector<String> 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<Fetch::Infrastructure::Request const> request, GC::Ref<Policy const> 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<Fetch::Infrastructure::Request const> request, GC::Ref<Fetch::Infrastructure::Response const> response, GC::Ref<Policy const> 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<DOM::Element const> element, InlineType type, GC::Ref<Policy const> 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright (c) 2024, Luke Wilde <luke@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LibWeb/ContentSecurityPolicy/Directives/Directive.h>
|
||||
|
||||
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<Fetch::Infrastructure::Request const>, GC::Ref<Policy const>) const override;
|
||||
virtual Result post_request_check(GC::Heap&, GC::Ref<Fetch::Infrastructure::Request const>, GC::Ref<Fetch::Infrastructure::Response const>, GC::Ref<Policy const>) const override;
|
||||
virtual Result inline_check(GC::Heap&, GC::Ptr<DOM::Element const>, InlineType, GC::Ref<Policy const>, String const&) const override;
|
||||
|
||||
private:
|
||||
ScriptSourceDirective(String name, Vector<String> value);
|
||||
};
|
||||
|
||||
}
|
|
@ -19,6 +19,7 @@
|
|||
#include <LibWeb/Bindings/EventTargetPrototype.h>
|
||||
#include <LibWeb/Bindings/MainThreadVM.h>
|
||||
#include <LibWeb/Bindings/PrincipalHostDefined.h>
|
||||
#include <LibWeb/ContentSecurityPolicy/BlockingAlgorithms.h>
|
||||
#include <LibWeb/DOM/AbortSignal.h>
|
||||
#include <LibWeb/DOM/DOMEventListener.h>
|
||||
#include <LibWeb/DOM/Document.h>
|
||||
|
@ -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<DOM::Element>(*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;
|
||||
|
|
|
@ -138,6 +138,7 @@ class ImageSourceDirective;
|
|||
class ManifestSourceDirective;
|
||||
class MediaSourceDirective;
|
||||
class ObjectSourceDirective;
|
||||
class ScriptSourceDirective;
|
||||
struct SerializedDirective;
|
||||
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ public:
|
|||
[[nodiscard]] GC::Ref<DOMStringMap> 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();
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
#include <LibTextCodec/Decoder.h>
|
||||
#include <LibWeb/Bindings/HTMLScriptElementPrototype.h>
|
||||
#include <LibWeb/Bindings/Intrinsics.h>
|
||||
#include <LibWeb/ContentSecurityPolicy/BlockingAlgorithms.h>
|
||||
#include <LibWeb/DOM/Document.h>
|
||||
#include <LibWeb/DOM/Event.h>
|
||||
#include <LibWeb/DOM/ShadowRoot.h>
|
||||
|
@ -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,
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
#include <LibJS/Runtime/Array.h>
|
||||
#include <LibJS/Runtime/TypedArray.h>
|
||||
#include <LibWeb/Bindings/MainThreadVM.h>
|
||||
#include <LibWeb/ContentSecurityPolicy/BlockingAlgorithms.h>
|
||||
#include <LibWeb/Crypto/Crypto.h>
|
||||
#include <LibWeb/Fetch/FetchMethod.h>
|
||||
#include <LibWeb/HTML/CanvasRenderingContext2D.h>
|
||||
|
@ -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<void()>([this, handler = move(handler), timeout, arguments = move(arguments), repeat, id, initiating_script, previous_id]() {
|
||||
auto task = GC::create_function(vm.heap(), Function<void()>([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<WebIDL::CallbackType> 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;
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
#include <LibWasm/AbstractMachine/Validator.h>
|
||||
#include <LibWeb/Bindings/Intrinsics.h>
|
||||
#include <LibWeb/Bindings/ResponsePrototype.h>
|
||||
#include <LibWeb/ContentSecurityPolicy/BlockingAlgorithms.h>
|
||||
#include <LibWeb/Fetch/Response.h>
|
||||
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
|
||||
#include <LibWeb/Platform/EventLoopPlugin.h>
|
||||
|
@ -149,6 +150,7 @@ WebIDL::ExceptionOr<GC::Ref<WebIDL::Promise>> 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<GC::Ref<WebIDL::Promise>> instantiate(JS::VM& vm, GC::Root<WebIDL::BufferSource>& bytes, Optional<GC::Root<JS::Object>>& import_object_handle)
|
||||
{
|
||||
auto& realm = *vm.current_realm();
|
||||
|
@ -160,10 +162,13 @@ WebIDL::ExceptionOr<GC::Ref<WebIDL::Promise>> instantiate(JS::VM& vm, GC::Root<W
|
|||
return WebIDL::create_rejected_promise_from_exception(realm, vm.throw_completion<JS::InternalError>(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<JS::Object> 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<NonnullOwnPtr<Wasm::ModuleInstance>> 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<NonnullRefPtr<CompiledWebAssemblyModule>> 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<void> 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
|
||||
|
|
|
@ -94,6 +94,7 @@ JS::NativeFunction* create_native_function(JS::VM&, Wasm::FunctionAddress addres
|
|||
JS::ThrowCompletionOr<Wasm::Value> 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<void> host_ensure_can_compile_wasm_bytes(JS::VM&);
|
||||
|
||||
extern HashMap<GC::Ptr<JS::Object>, WebAssemblyCache> s_caches;
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue