LibWeb/CSP: Introduce the ability to create and report a violation

A violation provides several details about an enforcement failing, such
as the URL of the document, the directive that returned "Blocked", etc.
This commit is contained in:
Luke Wilde 2024-11-25 17:22:08 +00:00 committed by Alexander Kalenik
parent 02236be737
commit 86170f4bfd
Notes: github-actions[bot] 2025-03-18 23:56:26 +00:00
12 changed files with 982 additions and 0 deletions

View file

@ -38,12 +38,14 @@ set(SOURCES
Compression/DecompressionStream.cpp
ContentSecurityPolicy/Directives/Directive.cpp
ContentSecurityPolicy/Directives/DirectiveFactory.cpp
ContentSecurityPolicy/Directives/DirectiveOperations.cpp
ContentSecurityPolicy/Directives/Names.cpp
ContentSecurityPolicy/Directives/SerializedDirective.cpp
ContentSecurityPolicy/Policy.cpp
ContentSecurityPolicy/PolicyList.cpp
ContentSecurityPolicy/SecurityPolicyViolationEvent.cpp
ContentSecurityPolicy/SerializedPolicy.cpp
ContentSecurityPolicy/Violation.cpp
CredentialManagement/Credential.cpp
CredentialManagement/CredentialsContainer.cpp
CredentialManagement/FederatedCredential.cpp

View file

@ -0,0 +1,193 @@
/*
* Copyright (c) 2025, Luke Wilde <luke@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/FlyString.h>
#include <AK/HashMap.h>
#include <AK/Vector.h>
#include <LibWeb/ContentSecurityPolicy/Directives/DirectiveOperations.h>
#include <LibWeb/ContentSecurityPolicy/Directives/Names.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Requests.h>
namespace Web::ContentSecurityPolicy::Directives {
// https://w3c.github.io/webappsec-csp/#directive-fallback-list
// Will return an ordered set of the fallback directives for a specific directive.
// The returned ordered set is sorted from most relevant to least relevant and it includes the effective directive
// itself.
static HashMap<StringView, Vector<StringView>> fetch_directive_fallback_list {
// "script-src-elem"
// 1. Return << "script-src-elem", "script-src", "default-src" >>.
{ "script-src-elem"sv, { "script-src-elem"sv, "script-src"sv, "default-src"sv } },
// "script-src-attr"
// 1. Return << "script-src-attr", "script-src", "default-src" >>.
{ "script-src-attr"sv, { "script-src-attr"sv, "script-src"sv, "default-src"sv } },
// "style-src-elem"
// 1. Return << "style-src-elem", "style-src", "default-src" >>.
{ "style-src-elem"sv, { "style-src-elem"sv, "style-src"sv, "default-src"sv } },
// "style-src-attr"
// 1. Return << "style-src-attr", "style-src", "default-src" >>.
{ "style-src-attr"sv, { "style-src-attr"sv, "style-src"sv, "default-src"sv } },
// "worker-src"
// 1. Return << "worker-src", "child-src", "script-src", "default-src" >>.
{ "worker-src"sv, { "worker-src"sv, "child-src"sv, "script-src"sv, "default-src"sv } },
// "connect-src"
// 1. Return << "connect-src", "default-src" >>.
{ "connect-src"sv, { "connect-src"sv, "default-src"sv } },
// "manifest-src"
// 1. Return << "manifest-src", "default-src" >>.
{ "manifest-src"sv, { "manifest-src"sv, "default-src"sv } },
// "object-src"
// 1. Return << "object-src", "default-src" >>.
{ "object-src"sv, { "object-src"sv, "default-src"sv } },
// "frame-src"
// 1. Return << "frame-src", "child-src", "default-src" >>.
{ "frame-src"sv, { "frame-src"sv, "child-src"sv, "default-src"sv } },
// "media-src"
// 1. Return << "media-src", "default-src" >>.
{ "media-src"sv, { "media-src"sv, "default-src"sv } },
// "font-src"
// 1. Return << "font-src", "default-src" >>.
{ "font-src"sv, { "font-src"sv, "default-src"sv } },
// "img-src"
// 1. Return << "img-src", "default-src" >>.
{ "img-src"sv, { "img-src"sv, "default-src"sv } },
};
// https://w3c.github.io/webappsec-csp/#effective-directive-for-a-request
Optional<FlyString> get_the_effective_directive_for_request(GC::Ref<Fetch::Infrastructure::Request const> request)
{
// Each fetch directive controls a specific destination of request. Given a request request, the following algorithm
// returns either null or the name of the requests effective directive:
// 1. If requests initiator is "prefetch" or "prerender", return default-src.
if (request->initiator() == Fetch::Infrastructure::Request::Initiator::Prefetch || request->initiator() == Fetch::Infrastructure::Request::Initiator::Prerender)
return Names::DefaultSrc;
// 2. Switch on requests destination, and execute the associated steps:
// the empty string
// 1. Return connect-src.
if (!request->destination().has_value())
return Names::ConnectSrc;
switch (request->destination().value()) {
// "manifest"
// 1. Return manifest-src.
case Fetch::Infrastructure::Request::Destination::Manifest:
return Names::ManifestSrc;
// "object"
// "embed"
// 1. Return object-src.
case Fetch::Infrastructure::Request::Destination::Object:
case Fetch::Infrastructure::Request::Destination::Embed:
return Names::ObjectSrc;
// "frame"
// "iframe"
// 1. Return frame-src.
case Fetch::Infrastructure::Request::Destination::Frame:
case Fetch::Infrastructure::Request::Destination::IFrame:
return Names::FrameSrc;
// "audio"
// "track"
// "video"
// 1. Return media-src.
case Fetch::Infrastructure::Request::Destination::Audio:
case Fetch::Infrastructure::Request::Destination::Track:
case Fetch::Infrastructure::Request::Destination::Video:
return Names::MediaSrc;
// "font"
// 1. Return font-src.
case Fetch::Infrastructure::Request::Destination::Font:
return Names::FontSrc;
// "image"
// 1. Return img-src.
case Fetch::Infrastructure::Request::Destination::Image:
return Names::ImgSrc;
// "style"
// 1. Return style-src-elem.
case Fetch::Infrastructure::Request::Destination::Style:
return Names::StyleSrcElem;
// "script"
// "xslt"
// "audioworklet"
// "paintworklet"
// 1. Return script-src-elem.
case Fetch::Infrastructure::Request::Destination::Script:
case Fetch::Infrastructure::Request::Destination::XSLT:
case Fetch::Infrastructure::Request::Destination::AudioWorklet:
case Fetch::Infrastructure::Request::Destination::PaintWorklet:
return Names::ScriptSrcElem;
// "serviceworker"
// "sharedworker"
// "worker"
// 1. Return worker-src.
case Fetch::Infrastructure::Request::Destination::ServiceWorker:
case Fetch::Infrastructure::Request::Destination::SharedWorker:
case Fetch::Infrastructure::Request::Destination::Worker:
return Names::WorkerSrc;
// "json"
// "webidentity"
// 1. Return connect-src.
case Fetch::Infrastructure::Request::Destination::JSON:
case Fetch::Infrastructure::Request::Destination::WebIdentity:
return Names::ConnectSrc;
// "report"
// 1. Return null.
case Fetch::Infrastructure::Request::Destination::Report:
return OptionalNone {};
// 3. Return connect-src.
// Spec Note: The algorithm returns connect-src as a default fallback. This is intended for new fetch destinations
// that are added and which dont explicitly fall into one of the other categories.
default:
return Names::ConnectSrc;
}
}
// https://w3c.github.io/webappsec-csp/#directive-fallback-list
Vector<StringView> get_fetch_directive_fallback_list(Optional<FlyString> directive_name)
{
if (!directive_name.has_value())
return {};
auto list_iterator = fetch_directive_fallback_list.find(directive_name.value());
if (list_iterator == fetch_directive_fallback_list.end())
return {};
return list_iterator->value;
}
// https://w3c.github.io/webappsec-csp/#should-directive-execute
ShouldExecute should_fetch_directive_execute(Optional<FlyString> effective_directive_name, FlyString const& directive_name, GC::Ref<Policy const> policy)
{
// 1. Let directive fallback list be the result of executing § 6.8.3 Get fetch directive fallback list on effective
// directive name.
auto const& directive_fallback_list = get_fetch_directive_fallback_list(effective_directive_name);
// 2. For each fallback directive of directive fallback list:
for (auto fallback_directive : directive_fallback_list) {
// 1. If directive name is fallback directive, Return "Yes".
if (directive_name == fallback_directive)
return ShouldExecute::Yes;
// 2. If policy contains a directive whose name is fallback directive, Return "No".
if (policy->contains_directive_with_name(fallback_directive))
return ShouldExecute::No;
}
// 3. Return "No".
return ShouldExecute::No;
}
}

View file

@ -0,0 +1,24 @@
/*
* Copyright (c) 2025, Luke Wilde <luke@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/StringView.h>
#include <LibGC/Ptr.h>
#include <LibWeb/Forward.h>
namespace Web::ContentSecurityPolicy::Directives {
enum class ShouldExecute {
No,
Yes,
};
[[nodiscard]] Optional<FlyString> get_the_effective_directive_for_request(GC::Ref<Fetch::Infrastructure::Request const> request);
[[nodiscard]] Vector<StringView> get_fetch_directive_fallback_list(Optional<FlyString> directive_name);
[[nodiscard]] ShouldExecute should_fetch_directive_execute(Optional<FlyString> effective_directive_name, FlyString const& directive_name, GC::Ref<Policy const> policy);
}

View file

@ -35,6 +35,7 @@ GC::Ref<Policy> Policy::parse_a_serialized_csp(JS::Realm& realm, Variant<ByteBuf
// 2. Let policy be a new policy with an empty directive set, a source of source, and a disposition of disposition.
auto policy = realm.create<Policy>();
policy->m_pre_parsed_policy_string = serialized_string;
policy->m_source = source;
policy->m_disposition = disposition;
@ -148,6 +149,7 @@ GC::Ref<Policy> Policy::create_from_serialized_policy(JS::Realm& realm, Serializ
policy->m_disposition = serialized_policy.disposition;
policy->m_source = serialized_policy.source;
policy->m_self_origin = serialized_policy.self_origin;
policy->m_pre_parsed_policy_string = serialized_policy.pre_parsed_policy_string;
return policy;
}
@ -159,6 +161,18 @@ bool Policy::contains_directive_with_name(StringView name) const
return !maybe_directive.is_end();
}
GC::Ptr<Directives::Directive> Policy::get_directive_by_name(StringView name) const
{
auto maybe_directive = m_directives.find_if([name](auto const& directive) {
return directive->name() == name;
});
if (!maybe_directive.is_end())
return *maybe_directive;
return nullptr;
}
GC::Ref<Policy> Policy::clone(JS::Realm& realm) const
{
auto policy = realm.create<Policy>();
@ -171,6 +185,7 @@ GC::Ref<Policy> Policy::clone(JS::Realm& realm) const
policy->m_disposition = m_disposition;
policy->m_source = m_source;
policy->m_self_origin = m_self_origin;
policy->m_pre_parsed_policy_string = m_pre_parsed_policy_string;
return policy;
}
@ -187,6 +202,7 @@ SerializedPolicy Policy::serialize() const
.disposition = m_disposition,
.source = m_source,
.self_origin = m_self_origin,
.pre_parsed_policy_string = m_pre_parsed_policy_string,
};
}

View file

@ -46,8 +46,10 @@ public:
[[nodiscard]] Disposition disposition() const { return m_disposition; }
[[nodiscard]] Source source() const { return m_source; }
[[nodiscard]] URL::Origin const& self_origin() const { return m_self_origin; }
[[nodiscard]] String const& pre_parsed_policy_string(Badge<Violation>) const { return m_pre_parsed_policy_string; }
[[nodiscard]] bool contains_directive_with_name(StringView name) const;
[[nodiscard]] GC::Ptr<Directives::Directive> get_directive_by_name(StringView) const;
[[nodiscard]] GC::Ref<Policy> clone(JS::Realm&) const;
[[nodiscard]] SerializedPolicy serialize() const;
@ -77,6 +79,12 @@ private:
// their policy but have an opaque origin. Most of the time this will simply be the environment settings
// objects origin.
URL::Origin m_self_origin;
// This is used for reporting which policy was violated. It's not exactly specified, only linking to an ABNF grammar
// definition. WebKit and Blink return the original string that was parsed, whereas Firefox seems to try and return
// a nice serialization of what it parsed. For simplicity and wider compatibility, we follow what WebKit and Blink
// do.
String m_pre_parsed_policy_string;
};
}

View file

@ -17,6 +17,7 @@ ErrorOr<void> encode(Encoder& encoder, Web::ContentSecurityPolicy::SerializedPol
TRY(encoder.encode(serialized_policy.disposition));
TRY(encoder.encode(serialized_policy.source));
TRY(encoder.encode(serialized_policy.self_origin));
TRY(encoder.encode(serialized_policy.pre_parsed_policy_string));
return {};
}
@ -30,6 +31,7 @@ ErrorOr<Web::ContentSecurityPolicy::SerializedPolicy> decode(Decoder& decoder)
serialized_policy.disposition = TRY(decoder.decode<Web::ContentSecurityPolicy::Policy::Disposition>());
serialized_policy.source = TRY(decoder.decode<Web::ContentSecurityPolicy::Policy::Source>());
serialized_policy.self_origin = TRY(decoder.decode<URL::Origin>());
serialized_policy.pre_parsed_policy_string = TRY(decoder.decode<String>());
return serialized_policy;
}

View file

@ -17,6 +17,7 @@ struct SerializedPolicy {
Policy::Disposition disposition;
Policy::Source source;
URL::Origin self_origin;
String pre_parsed_policy_string;
};
}

View file

@ -0,0 +1,480 @@
/*
* 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");
}
}
}));
}
}

View file

@ -0,0 +1,141 @@
/*
* Copyright (c) 2025, Luke Wilde <luke@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibGC/CellAllocator.h>
#include <LibJS/Heap/Cell.h>
#include <LibURL/URL.h>
#include <LibWeb/ContentSecurityPolicy/Policy.h>
#include <LibWeb/Forward.h>
namespace Web::ContentSecurityPolicy {
#define ENUMERATE_RESOURCE_TYPES \
__ENUMERATE_RESOURCE_TYPE(Inline, "inline") \
__ENUMERATE_RESOURCE_TYPE(Eval, "eval") \
__ENUMERATE_RESOURCE_TYPE(WasmEval, "wasm-eval") \
__ENUMERATE_RESOURCE_TYPE(TrustedTypesPolicy, "trusted-types-policy") \
__ENUMERATE_RESOURCE_TYPE(TrustedTypesSink, "trusted-types-sink")
// https://w3c.github.io/webappsec-csp/#violation
// A violation represents an action or resource which goes against the set of policy objects associated with a global
// object.
class Violation final : public JS::Cell {
GC_CELL(Violation, JS::Cell);
GC_DECLARE_ALLOCATOR(Violation);
public:
enum class Resource {
#define __ENUMERATE_RESOURCE_TYPE(type, _) type,
ENUMERATE_RESOURCE_TYPES
#undef __ENUMERATE_RESOURCE_TYPE
};
using ResourceType = Variant<Empty, Resource, URL::URL>;
virtual ~Violation() = default;
[[nodiscard]] static GC::Ref<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);
[[nodiscard]] static GC::Ref<Violation> create_a_violation_object_for_request_and_policy(JS::Realm& realm, GC::Ref<Fetch::Infrastructure::Request> request, GC::Ref<Policy const>);
// https://w3c.github.io/webappsec-csp/#violation-url
[[nodiscard]] URL::URL url() const;
[[nodiscard]] u16 status() const { return m_status; }
void set_status(u16 status) { m_status = status; }
[[nodiscard]] ResourceType const& resource() const { return m_resource; }
void set_resource(ResourceType resource) { m_resource = resource; }
[[nodiscard]] Optional<URL::URL> const& referrer() const { return m_referrer; }
[[nodiscard]] Policy const& policy() const { return m_policy; }
// https://w3c.github.io/webappsec-csp/#violation-disposition
[[nodiscard]] Policy::Disposition disposition() const { return m_policy->disposition(); }
[[nodiscard]] String const& effective_directive() const { return m_effective_directive; }
[[nodiscard]] Optional<URL::URL> source_file() const { return m_source_file; }
void set_source_file(URL::URL source_file) { m_source_file = source_file; }
[[nodiscard]] u32 line_number() const { return m_line_number; }
void set_line_number(u32 line_number) { m_line_number = line_number; }
[[nodiscard]] u32 column_number() const { return m_column_number; }
void set_column_number(u32 column_number) { m_column_number = column_number; }
[[nodiscard]] GC::Ptr<DOM::Element> element() const { return m_element; }
void set_element(GC::Ref<DOM::Element> element) { m_element = element; }
[[nodiscard]] String const& sample() const { return m_sample; }
void set_sample(String sample) { m_sample = sample; }
void report_a_violation(JS::Realm&);
protected:
virtual void visit_edges(Cell::Visitor&) override;
private:
Violation(GC::Ptr<JS::Object> global_object, GC::Ref<Policy const> policy, String directive);
[[nodiscard]] String obtain_the_blocked_uri_of_resource() const;
[[nodiscard]] ByteBuffer obtain_the_deprecated_serialization(JS::Realm&) const;
// https://w3c.github.io/webappsec-csp/#violation-global-object
// Each violation has a global object, which is the global object whose policy has been violated.
GC::Ptr<JS::Object> m_global_object;
// https://w3c.github.io/webappsec-csp/#violation-status
// Each violation has a status which is a non-negative integer representing the HTTP status code of the resource
// for which the global object was instantiated.
u16 m_status { 0 };
// https://w3c.github.io/webappsec-csp/#violation-resource
// Each violation has a resource, which is either null, "inline", "eval", "wasm-eval", "trusted-types-policy"
// "trusted-types-sink" or a URL. It represents the resource which violated the policy.
// Spec Note: The value null for a violations resource is only allowed while the violation is being populated.
// By the time the violation is reported and its resource is used for obtaining the blocked URI, the
// violations resource should be populated with a URL or one of the allowed strings.
ResourceType m_resource;
// https://w3c.github.io/webappsec-csp/#violation-referrer
// Each violation has a referrer, which is either null, or a URL. It represents the referrer of the resource whose
// policy was violated.
Optional<URL::URL> m_referrer;
// https://w3c.github.io/webappsec-csp/#violation-policy
// Each violation has a policy, which is the policy that has been violated.
GC::Ref<Policy const> m_policy;
// https://w3c.github.io/webappsec-csp/#violation-effective-directive
// Each violation has an effective directive which is a non-empty string representing the directive whose enforcement
// caused the violation.
String m_effective_directive;
// https://w3c.github.io/webappsec-csp/#violation-source-file
// Each violation has a source file, which is either null or a URL.
Optional<URL::URL> m_source_file;
// https://w3c.github.io/webappsec-csp/#violation-line-number
// Each violation has a line number, which is a non-negative integer.
u32 m_line_number { 0 };
// https://w3c.github.io/webappsec-csp/#violation-column-number
// Each violation has a column number, which is a non-negative integer.
u32 m_column_number { 0 };
// https://w3c.github.io/webappsec-csp/#violation-element
// Each violation has a element, which is either null or an element.
GC::Ptr<DOM::Element> m_element;
// https://w3c.github.io/webappsec-csp/#violation-sample
// Each violation has a sample, which is a string. It is the empty string unless otherwise specified.
String m_sample;
};
}

View file

@ -103,6 +103,7 @@ namespace Web::ContentSecurityPolicy {
class Policy;
class PolicyList;
class SecurityPolicyViolationEvent;
class Violation;
struct SecurityPolicyViolationEventInit;
struct SerializedPolicy;
}

View file

@ -6,6 +6,7 @@
#include <AK/String.h>
#include <LibJS/Runtime/AbstractOperations.h>
#include <LibJS/Runtime/Array.h>
#include <LibJS/Runtime/Completion.h>
#include <LibJS/Runtime/Value.h>
#include <LibTextCodec/Decoder.h>
@ -66,4 +67,108 @@ WebIDL::ExceptionOr<ByteBuffer> serialize_javascript_value_to_json_bytes(JS::VM&
return TRY_OR_THROW_OOM(vm, ByteBuffer::copy(string.bytes()));
}
// https://infra.spec.whatwg.org/#convert-an-infra-value-to-a-json-compatible-javascript-value
[[nodiscard]] static JS::Value convert_an_infra_value_to_a_json_compatible_javascript_value(JS::Realm& realm, JSONTopLevel const& value)
{
auto& vm = realm.vm();
if (value.has<JSONValue>()) {
// 1. If value is a string, boolean, number, or null, then return value.
auto const& json_value = value.get<JSONValue>();
if (json_value.has<JSONBaseValue>()) {
auto const& base_value = json_value.get<JSONBaseValue>();
if (base_value.has<String>())
return JS::PrimitiveString::create(vm, base_value.get<String>());
if (base_value.has<bool>())
return JS::Value(base_value.get<bool>());
if (base_value.has<u16>())
return JS::Value(base_value.get<u16>());
if (base_value.has<u32>())
return JS::Value(base_value.get<u32>());
VERIFY(base_value.has<Empty>());
return JS::js_null();
}
// 2. If value is a list, then:
VERIFY(json_value.has<Vector<JSONBaseValue>>());
auto const& list_value = json_value.get<Vector<JSONBaseValue>>();
// 1. Let jsValue be ! ArrayCreate(0).
auto js_value = MUST(JS::Array::create(realm, 0));
// 2. Let i be 0.
u64 index = 0;
// 3. For each listItem of value:
for (auto const& list_item : list_value) {
// 1. Let listItemJSValue be the result of converting an Infra value to a JSON-compatible JavaScript value,
// given listItem.
auto list_item_js_value = convert_an_infra_value_to_a_json_compatible_javascript_value(realm, JSONValue { list_item });
// 2. Perform ! CreateDataPropertyOrThrow(jsValue, ! ToString(i), listItemJSValue).
MUST(js_value->create_data_property_or_throw(index, list_item_js_value));
// 3. Set i to i + 1.
++index;
}
// 4. Return jsValue.
return js_value;
}
// 3. Assert: value is a map.
VERIFY(value.has<JSONObject>());
auto const& map_value = value.get<JSONObject>();
// 4. Let jsValue be ! OrdinaryObjectCreate(null).
auto js_value = JS::Object::create(realm, nullptr);
// 5. For each mapKey → mapValue of value:
for (auto const& map_entry : map_value.value) {
// 1. Assert: mapKey is a string.
// 2. Let mapValueJSValue be the result of converting an Infra value to a JSON-compatible JavaScript value,
// given mapValue.
auto map_value_js_value = convert_an_infra_value_to_a_json_compatible_javascript_value(realm, map_entry.value);
// 3. Perform ! CreateDataPropertyOrThrow(jsValue, mapKey, mapValueJSValue).
MUST(js_value->create_data_property_or_throw(map_entry.key.to_byte_string(), map_value_js_value));
}
// 6. Return jsValue.
return js_value;
}
// https://infra.spec.whatwg.org/#serialize-an-infra-value-to-a-json-string
String serialize_an_infra_value_to_a_json_string(JS::Realm& realm, JSONTopLevel const& value)
{
auto& vm = realm.vm();
// 1. Let jsValue be the result of converting an Infra value to a JSON-compatible JavaScript value, given value.
auto js_value = convert_an_infra_value_to_a_json_compatible_javascript_value(realm, value);
// 2. Return ! Call(%JSON.stringify%, undefined, « jsValue »).
// Spec Note: Since no additional arguments are passed to %JSON.stringify%, the resulting string will have no
// whitespace inserted.
auto result = MUST(JS::call(vm, *realm.intrinsics().json_stringify_function(), JS::js_undefined(), js_value));
VERIFY(result.is_string());
return result.as_string().utf8_string();
}
// https://infra.spec.whatwg.org/#serialize-a-javascript-value-to-json-bytes
ByteBuffer serialize_an_infra_value_to_json_bytes(JS::Realm& realm, JSONTopLevel const& value)
{
// 1. Let string be the result of serializing an Infra value to a JSON string, given value.
auto string = serialize_an_infra_value_to_a_json_string(realm, value);
// 2. Return the result of running UTF-8 encode on string. [ENCODING]
// NOTE: LibJS strings are stored as UTF-8.
return MUST(ByteBuffer::copy(string.bytes()));
}
}

View file

@ -12,9 +12,18 @@
namespace Web::Infra {
using JSONBaseValue = Variant<Empty, u16, u32, bool, String>;
using JSONValue = Variant<JSONBaseValue, Vector<JSONBaseValue>>;
struct JSONObject {
OrderedHashMap<String, Variant<JSONValue, JSONObject>> value;
};
using JSONTopLevel = Variant<JSONValue, JSONObject>;
WebIDL::ExceptionOr<JS::Value> parse_json_string_to_javascript_value(JS::Realm&, StringView);
WebIDL::ExceptionOr<JS::Value> parse_json_bytes_to_javascript_value(JS::Realm&, ReadonlyBytes);
WebIDL::ExceptionOr<String> serialize_javascript_value_to_json_string(JS::VM&, JS::Value);
WebIDL::ExceptionOr<ByteBuffer> serialize_javascript_value_to_json_bytes(JS::VM&, JS::Value);
String serialize_an_infra_value_to_a_json_string(JS::Realm&, JSONTopLevel const&);
ByteBuffer serialize_an_infra_value_to_json_bytes(JS::Realm&, JSONTopLevel const&);
}