LibWeb/CSP: Implement the script-src directive

This commit is contained in:
Luke Wilde 2024-12-02 16:01:19 +00:00 committed by Andrew Kaster
commit 0cff47828d
Notes: github-actions[bot] 2025-07-09 21:53:59 +00:00
17 changed files with 839 additions and 14 deletions

View file

@ -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 elements Document's global objects 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 policys directive set:
for (auto const directive : policy->directives()) {
// 1. If directives 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 objects 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 violations resource to "inline".
violation->set_resource(Violation::Resource::Inline);
// 5. Set violations element to element.
violation->set_element(element);
// 6. If directives value contains the expression "'report-sample'", then set violations 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 policys 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 realms global object.
auto& global = realm.global_object();
// 5. For each policy of globals 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
// directives 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 violations resource to "eval".
violation->set_resource(Violation::Resource::Eval);
// 3. If source-list contains the expression "'report-sample'", then set violations 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 policys 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 realms global object.
auto& global = realm.global_object();
// 2. Let result be "Allowed".
auto result = Directives::Directive::Result::Allowed;
// 3. For each policy of globals 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
// directives 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 violations 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 policys 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 {};
}
}