ladybird/Libraries/LibWeb/ContentSecurityPolicy/Violation.cpp

481 lines
22 KiB
C++
Raw Normal View History

/*
* Copyright (c) 2025, Luke Wilde <luke@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/ByteBuffer.h>
#include <LibURL/Parser.h>
#include <LibURL/URL.h>
#include <LibWeb/Bindings/PrincipalHostDefined.h>
#include <LibWeb/ContentSecurityPolicy/Directives/DirectiveOperations.h>
#include <LibWeb/ContentSecurityPolicy/Directives/Names.h>
#include <LibWeb/ContentSecurityPolicy/SecurityPolicyViolationEvent.h>
#include <LibWeb/ContentSecurityPolicy/Violation.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOMURL/DOMURL.h>
#include <LibWeb/Fetch/Fetching/Fetching.h>
#include <LibWeb/Fetch/Infrastructure/URL.h>
#include <LibWeb/HTML/Scripting/Environments.h>
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/HTML/WorkerGlobalScope.h>
#include <LibWeb/Infra/JSON.h>
namespace Web::ContentSecurityPolicy {
GC_DEFINE_ALLOCATOR(Violation);
Violation::Violation(GC::Ptr<JS::Object> global_object, GC::Ref<Policy const> policy, String directive)
: m_global_object(global_object)
, m_policy(policy)
, m_effective_directive(directive)
{
}
// https://w3c.github.io/webappsec-csp/#create-violation-for-global
GC::Ref<Violation> Violation::create_a_violation_object_for_global_policy_and_directive(JS::Realm& realm, GC::Ptr<JS::Object> global_object, GC::Ref<Policy const> policy, String directive)
{
// 1. Let violation be a new violation whose global object is global, policy is policy, effective directive is
// directive, and resource is null.
auto violation = realm.create<Violation>(global_object, policy, directive);
// FIXME: 2. If the user agent is currently executing script, and can extract a source files URL, line number,
// and column number from the global, set violations source file, line number, and column number
// accordingly.
// SPEC ISSUE 1: Is this kind of thing specified anywhere? I didnt see anything that looked useful in [ECMA262].
// 3. If global is a Window object, set violations referrer to globals document's referrer.
if (global_object) {
if (auto* window = dynamic_cast<HTML::Window*>(global_object.ptr())) {
violation->m_referrer = URL::Parser::basic_parse(window->associated_document().referrer());
}
}
// FIXME: 4. Set violations status to the HTTP status code for the resource associated with violations global object.
// SPEC ISSUE 2: How, exactly, do we get the status code? We dont actually store it anywhere.
// 5. Return violation.
return violation;
}
// https://w3c.github.io/webappsec-csp/#create-violation-for-request
GC::Ref<Violation> Violation::create_a_violation_object_for_request_and_policy(JS::Realm& realm, GC::Ref<Fetch::Infrastructure::Request> request, GC::Ref<Policy const> policy)
{
// 1. Let directive be the result of executing § 6.8.1 Get the effective directive for request on request.
auto directive = Directives::get_the_effective_directive_for_request(request);
// NOTE: The spec assumes that the effective directive of a Violation is a non-empty string.
// See the definition of m_effective_directive.
VERIFY(directive.has_value());
// 2. Let violation be the result of executing § 2.4.1 Create a violation object for global, policy, and directive
// on requests clients global object, policy, and directive.
auto violation = create_a_violation_object_for_global_policy_and_directive(realm, request->client()->global_object(), policy, directive->to_string());
// 3. Set violations resource to requests url.
// Spec Note: We use requests url, and not its current url, as the latter might contain information about redirect
// targets to which the page MUST NOT be given access.
violation->m_resource = request->url();
// 4. Return violation.
return violation;
}
void Violation::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_global_object);
visitor.visit(m_policy);
visitor.visit(m_element);
}
// https://w3c.github.io/webappsec-csp/#violation-url
URL::URL Violation::url() const
{
// Each violation has a url which is its global objects URL.
if (!m_global_object) {
// FIXME: What do we return here?
dbgln("FIXME: Figure out URL for violation with null global object.");
return URL::URL {};
}
// FIXME: File a spec issue about what to do for ShadowRealms here.
auto* universal_scope = dynamic_cast<HTML::UniversalGlobalScopeMixin*>(m_global_object.ptr());
VERIFY(universal_scope);
auto& principal_global = HTML::relevant_principal_global_object(universal_scope->this_impl());
if (auto* window = dynamic_cast<HTML::Window*>(&principal_global)) {
return window->associated_document().url();
}
if (auto* worker = dynamic_cast<HTML::WorkerGlobalScope*>(&principal_global)) {
return worker->url();
}
TODO();
}
// https://w3c.github.io/webappsec-csp/#strip-url-for-use-in-reports
[[nodiscard]] static String strip_url_for_use_in_reports(URL::URL url)
{
// 1. If urls scheme is not an HTTP(S) scheme, then return urls scheme.
if (!Fetch::Infrastructure::is_http_or_https_scheme(url.scheme()))
return url.scheme();
// 2. Set urls fragment to the empty string.
// FIXME: File spec issue about potentially meaning `null` here, as using empty string leaves a stray # at the end.
url.set_fragment(OptionalNone {});
// 3. Set urls username to the empty string.
url.set_username(String {});
// 4. Set urls password to the empty string.
url.set_password(String {});
// 5. Return the result of executing the URL serializer on url.
return url.serialize();
}
// https://w3c.github.io/webappsec-csp/#obtain-violation-blocked-uri
String Violation::obtain_the_blocked_uri_of_resource() const
{
// 1. Assert: resource is a URL or a string.
VERIFY(m_resource.has<URL::URL>() || m_resource.has<Resource>());
// 2. If resource is a URL, return the result of executing § 5.4 Strip URL for use in reports on resource.
if (m_resource.has<URL::URL>()) {
auto const& url = m_resource.get<URL::URL>();
return strip_url_for_use_in_reports(url);
}
// 3. Return resource.
auto resource = m_resource.get<Resource>();
switch (resource) {
#define __ENUMERATE_RESOURCE_TYPE(type, value) \
case Resource::type: \
return value##_string;
ENUMERATE_RESOURCE_TYPES
#undef __ENUMERATE_RESOURCE_TYPE
default:
VERIFY_NOT_REACHED();
}
}
[[nodiscard]] static String original_disposition_to_string(Policy::Disposition disposition)
{
switch (disposition) {
#define __ENUMERATE_DISPOSITION_TYPE(type, value) \
case Policy::Disposition::type: \
return value##_string;
ENUMERATE_DISPOSITION_TYPES
#undef __ENUMERATE_DISPOSITION_TYPE
default:
VERIFY_NOT_REACHED();
}
}
// https://w3c.github.io/webappsec-csp/#deprecated-serialize-violation
ByteBuffer Violation::obtain_the_deprecated_serialization(JS::Realm& realm) const
{
// 1. Let body be a map with its keys initialized as follows:
Infra::JSONObject body;
// "document-uri"
// The result of executing § 5.4 Strip URL for use in reports on violation's url.
body.value.set("document-uri"_string, Infra::JSONValue { strip_url_for_use_in_reports(url()) });
// "referrer"
// The result of executing § 5.4 Strip URL for use in reports on violation's referrer.
// FIXME: File spec issue that referrer can be null here.
Infra::JSONValue referrer = m_referrer.has_value()
? Infra::JSONValue { strip_url_for_use_in_reports(m_referrer.value()) }
: Infra::JSONValue { Empty {} };
body.value.set("referrer"_string, referrer);
// "blocked-uri"
// The result of executing § 5.2 Obtain the blockedURI of a violations resource on violations resource.
body.value.set("blocked_uri"_string, Infra::JSONValue { obtain_the_blocked_uri_of_resource() });
// "effective-directive"
// violation's effective directive
body.value.set("effective-directive"_string, Infra::JSONValue { m_effective_directive });
// "violated-directive"
// violation's effective directive
body.value.set("violated-directive"_string, Infra::JSONValue { m_effective_directive });
// "original-policy"
// The serialization of violation's policy
body.value.set("original-policy"_string, Infra::JSONValue { m_policy->pre_parsed_policy_string({}) });
// "disposition"
// The disposition of violation's policy
body.value.set("disposition"_string, Infra::JSONValue { original_disposition_to_string(disposition()) });
// "status-code"
// violation's status
body.value.set("status-code"_string, Infra::JSONValue { m_status });
// "script-sample"
// violation's sample
// Spec Note: The name script-sample was chosen for compatibility with an earlier iteration of this feature which
// has shipped in Firefox since its initial implementation of CSP. Despite the name, this field will
// contain samples for non-script violations, like stylesheets. The data contained in a
// SecurityPolicyViolationEvent object, and in reports generated via the new report-to directive, is
// named in a more encompassing fashion: sample.
body.value.set("script-sample"_string, Infra::JSONValue { m_sample });
// 2. If violations source file is not null:
if (m_source_file.has_value()) {
// 1. Set body["source-file'] to the result of executing § 5.4 Strip URL for use in reports on violations
// source file.
body.value.set("source-file"_string, Infra::JSONValue { strip_url_for_use_in_reports(m_source_file.value()) });
// 2. Set body["line-number"] to violations line number.
body.value.set("line-number"_string, Infra::JSONValue { m_line_number });
// 3. Set body["column-number"] to violations column number.
body.value.set("column-number"_string, Infra::JSONValue { m_column_number });
}
// 3. Assert: If body["blocked-uri"] is not "inline", then body["sample"] is the empty string.
// FIXME: File spec issue that body["sample"] should be body["script-sample"]
if (m_resource.has<Resource>() && m_resource.get<Resource>() != Resource::Inline) {
VERIFY(m_sample.is_empty());
}
// 4. Return the result of serialize an infra value to JSON bytes given «[ "csp-report" → body ]».
Infra::JSONObject csp_report;
csp_report.value.set("csp-report"_string, Infra::JSONObject { move(body) });
HTML::TemporaryExecutionContext execution_context { realm };
return Infra::serialize_an_infra_value_to_json_bytes(realm, move(csp_report));
}
[[nodiscard]] static Bindings::SecurityPolicyViolationEventDisposition original_disposition_to_bindings_disposition(Policy::Disposition disposition)
{
switch (disposition) {
#define __ENUMERATE_DISPOSITION_TYPE(type, _) \
case Policy::Disposition::type: \
return Bindings::SecurityPolicyViolationEventDisposition::type;
ENUMERATE_DISPOSITION_TYPES
#undef __ENUMERATE_DISPOSITION_TYPE
default:
VERIFY_NOT_REACHED();
}
}
// https://w3c.github.io/webappsec-csp/#report-violation
void Violation::report_a_violation(JS::Realm& realm)
{
dbgln("Content Security Policy violation{}: Refusing access to resource '{}' because it does not appear in the '{}' directive.",
disposition() == Policy::Disposition::Report ? " (report only)"sv : ""sv,
obtain_the_blocked_uri_of_resource(),
m_effective_directive);
// 1. Let global be violations global object.
auto global = m_global_object;
// 2. Let target be violations element.
auto target = m_element;
// 3. Queue a task to run the following steps:
// Spec Note: We "queue a task" here to ensure that the event targeting and dispatch happens after JavaScript
// completes execution of the task responsible for a given violation (which might manipulate the DOM).
HTML::queue_a_task(HTML::Task::Source::Unspecified, nullptr, nullptr, GC::create_function(realm.heap(), [this, global, target, &realm] {
auto& vm = realm.vm();
GC::Ptr<JS::Object> target_as_object = target;
// 1. If target is not null, and global is a Window, and targets shadow-including root is not globals
// associated Document, set target to null.
// Spec Note: This ensures that we fire events only at elements connected to violations policys Document.
// If a violation is caused by an element which isnt connected to that document, well fire the
// event at the document rather than the element in order to ensure that the violation is visible
// to the documents listeners.
if (target && is<HTML::Window>(global.ptr())) {
auto const& window = static_cast<HTML::Window const&>(*global.ptr());
if (&target->shadow_including_root() != &window.associated_document())
target_as_object = nullptr;
}
// 2. If target is null:
if (!target_as_object) {
// 1. Set target to violations global object.
target_as_object = m_global_object;
// 2. If target is a Window, set target to targets associated Document.
if (is<HTML::Window>(target_as_object.ptr())) {
auto& window = static_cast<HTML::Window&>(*target_as_object.ptr());
target_as_object = window.associated_document();
}
}
// 3. If target implements EventTarget, fire an event named securitypolicyviolation that uses the
// SecurityPolicyViolationEvent interface at target with its attributes initialized as follows:
if (is<DOM::EventTarget>(target_as_object.ptr())) {
auto& event_target = static_cast<DOM::EventTarget&>(*target_as_object.ptr());
SecurityPolicyViolationEventInit event_init {};
// bubbles
// true
event_init.bubbles = true;
// composed
// true
// Spec Note: We set the composed attribute, which means that this event can be captured on its way
// into, and will bubble its way out of a shadow tree. target, et al will be automagically
// scoped correctly for the main tree.
event_init.composed = true;
// documentURI
// The result of executing § 5.4 Strip URL for use in reports on violation's url.
event_init.document_uri = strip_url_for_use_in_reports(url());
// referrer
// The result of executing § 5.4 Strip URL for use in reports on violation's referrer.
// FIXME: File spec issue for referrer being potentially null.
event_init.referrer = m_referrer.has_value() ? strip_url_for_use_in_reports(m_referrer.value()) : String {};
// blockedURI
// The result of executing § 5.2 Obtain the blockedURI of a violation's resource on violations
// resource.
event_init.blocked_uri = obtain_the_blocked_uri_of_resource();
// effectiveDirective
// violation's effective directive
event_init.effective_directive = m_effective_directive;
// violatedDirective
// violation's effective directive
// Spec Note: Both effectiveDirective and violatedDirective are the same value. This is intentional
// to maintain backwards compatibility.
event_init.violated_directive = m_effective_directive;
// originalPolicy
// The serialization of violation's policy
event_init.original_policy = m_policy->pre_parsed_policy_string({});
// disposition
// violation's disposition
event_init.disposition = original_disposition_to_bindings_disposition(disposition());
// sourceFile
// The result of executing § 5.4 Strip URL for use in reports on violations source file, if
// violation's source file is not null, or null otherwise.
event_init.source_file = m_source_file.has_value() ? strip_url_for_use_in_reports(m_source_file.value()) : String {};
// statusCode
// violation's status
event_init.status_code = m_status;
// lineNumber
// violations line number
event_init.line_number = m_line_number;
// columnNumber
// violations column number
event_init.column_number = m_column_number;
// sample
// violation's sample
event_init.sample = m_sample;
auto event = SecurityPolicyViolationEvent::create(realm, HTML::EventNames::securitypolicyviolation, event_init);
event->set_is_trusted(true);
event_target.dispatch_event(event);
}
// 4. If violations policys directive set contains a directive named "report-uri" directive:
if (auto report_uri_directive = m_policy->get_directive_by_name(Directives::Names::ReportUri)) {
// 1. If violations policys directive set contains a directive named "report-to", skip the remaining
// substeps.
if (!m_policy->contains_directive_with_name(Directives::Names::ReportTo)) {
// 1. For each token of directives value:
for (auto const& token : report_uri_directive->value()) {
// 1. Let endpoint be the result of executing the URL parser with token as the input, and
// violations url as the base URL.
auto endpoint = DOMURL::parse(token, url());
// 2. If endpoint is not a valid URL, skip the remaining substeps.
if (endpoint.has_value()) {
// 3. Let request be a new request, initialized as follows:
auto request = Fetch::Infrastructure::Request::create(vm);
// method
// "POST"
request->set_method(MUST(ByteBuffer::copy("POST"sv.bytes())));
// url
// violations url
// FIXME: File spec issue that this is incorrect, it should be `endpoint` instead.
request->set_url(endpoint.value());
// origin
// violation's global object's relevant settings object's origin
// FIXME: File spec issue that global object can be null, so we use the realm to get the ESO
// instead, and cross ShadowRealm boundaries with the principal realm.
auto& environment_settings_object = Bindings::principal_host_defined_environment_settings_object(HTML::principal_realm(realm));
request->set_origin(environment_settings_object.origin());
// window
// "no-window"
request->set_window(Fetch::Infrastructure::Request::Window::NoWindow);
// client
// violation's global object's relevant settings object
request->set_client(&environment_settings_object);
// destination
// "report"
request->set_destination(Fetch::Infrastructure::Request::Destination::Report);
// initiator
// ""
request->set_initiator(OptionalNone {});
// credentials mode
// "same-origin"
request->set_credentials_mode(Fetch::Infrastructure::Request::CredentialsMode::SameOrigin);
// keepalive
// "true"
request->set_keepalive(true);
// header list
// A header list containing a single header whose name is "Content-Type", and value is
// "application/csp-report"
auto header_list = Fetch::Infrastructure::HeaderList::create(vm);
auto content_type_header = Fetch::Infrastructure::Header::from_string_pair("Content-Type"sv, "application/csp-report"sv);
header_list->append(move(content_type_header));
request->set_header_list(header_list);
// body
// The result of executing § 5.3 Obtain the deprecated serialization of violation on
// violation
request->set_body(obtain_the_deprecated_serialization(realm));
// redirect mode
// "error"
request->set_redirect_mode(Fetch::Infrastructure::Request::RedirectMode::Error);
// 4. Fetch request. The result will be ignored.
(void)Fetch::Fetching::fetch(realm, request, Fetch::Infrastructure::FetchAlgorithms::create(vm, {}));
}
}
}
// 5. If violation's policy's directive set contains a directive named "report-to" directive:
if (auto report_to_directive = m_policy->get_directive_by_name(Directives::Names::ReportTo)) {
(void)report_to_directive;
dbgln("FIXME: Implement report-to directive in violation reporting");
}
}
}));
}
}