/* * Copyright (c) 2025, Luke Wilde * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Web::ContentSecurityPolicy { GC_DEFINE_ALLOCATOR(Violation); Violation::Violation(GC::Ptr global_object, GC::Ref 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::create_a_violation_object_for_global_policy_and_directive(JS::Realm& realm, GC::Ptr global_object, GC::Ref 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(global_object, policy, directive); // FIXME: 2. If the user agent is currently executing script, and can extract a source file’s URL, line number, // and column number from the global, set violation’s source file, line number, and column number // accordingly. // SPEC ISSUE 1: Is this kind of thing specified anywhere? I didn’t see anything that looked useful in [ECMA262]. // 3. If global is a Window object, set violation’s referrer to global’s document's referrer. if (global_object) { if (auto* window = dynamic_cast(global_object.ptr())) { violation->m_referrer = URL::Parser::basic_parse(window->associated_document().referrer()); } } // FIXME: 4. Set violation’s status to the HTTP status code for the resource associated with violation’s global object. // SPEC ISSUE 2: How, exactly, do we get the status code? We don’t actually store it anywhere. // 5. Return violation. return violation; } // https://w3c.github.io/webappsec-csp/#create-violation-for-request GC::Ref Violation::create_a_violation_object_for_request_and_policy(JS::Realm& realm, GC::Ref request, GC::Ref 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 request’s client’s 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 violation’s resource to request’s url. // Spec Note: We use request’s 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 object’s 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(m_global_object.ptr()); VERIFY(universal_scope); auto& principal_global = HTML::relevant_principal_global_object(universal_scope->this_impl()); if (auto* window = dynamic_cast(&principal_global)) { return window->associated_document().url(); } if (auto* worker = dynamic_cast(&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 url’s scheme is not an HTTP(S) scheme, then return url’s scheme. if (!Fetch::Infrastructure::is_http_or_https_scheme(url.scheme())) return url.scheme(); // 2. Set url’s 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 url’s username to the empty string. url.set_username(String {}); // 4. Set url’s 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() || m_resource.has()); // 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()) { auto const& url = m_resource.get(); return strip_url_for_use_in_reports(url); } // 3. Return resource. auto resource = m_resource.get(); 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 violation’s resource on violation’s 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 violation’s 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 violation’s // 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 violation’s line number. body.value.set("line-number"_string, Infra::JSONValue { m_line_number }); // 3. Set body["column-number"] to violation’s 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() && m_resource.get() != 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 violation’s global object. auto global = m_global_object; // 2. Let target be violation’s 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 target_as_object = target; // 1. If target is not null, and global is a Window, and target’s shadow-including root is not global’s // associated Document, set target to null. // Spec Note: This ensures that we fire events only at elements connected to violation’s policy’s Document. // If a violation is caused by an element which isn’t connected to that document, we’ll fire the // event at the document rather than the element in order to ensure that the violation is visible // to the document’s listeners. if (target && is(global.ptr())) { auto const& window = static_cast(*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 violation’s global object. target_as_object = m_global_object; // 2. If target is a Window, set target to target’s associated Document. if (is(target_as_object.ptr())) { auto& window = static_cast(*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(target_as_object.ptr())) { auto& event_target = static_cast(*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 violation’s // 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 violation’s 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 // violation’s line number event_init.line_number = m_line_number; // columnNumber // violation’s 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 violation’s policy’s 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 violation’s policy’s 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 directive’s 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 // violation’s 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 // violation’s 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"); } } })); } }