mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-04-20 03:25:13 +00:00
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:
parent
02236be737
commit
86170f4bfd
Notes:
github-actions[bot]
2025-03-18 23:56:26 +00:00
Author: https://github.com/Lubrsi Commit: https://github.com/LadybirdBrowser/ladybird/commit/86170f4bfd3 Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/3972
12 changed files with 982 additions and 0 deletions
|
@ -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
|
||||
|
|
|
@ -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 request’s effective directive:
|
||||
// 1. If request’s 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 request’s 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 don’t 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
// object’s 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;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ struct SerializedPolicy {
|
|||
Policy::Disposition disposition;
|
||||
Policy::Source source;
|
||||
URL::Origin self_origin;
|
||||
String pre_parsed_policy_string;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
480
Libraries/LibWeb/ContentSecurityPolicy/Violation.cpp
Normal file
480
Libraries/LibWeb/ContentSecurityPolicy/Violation.cpp
Normal 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 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<HTML::Window*>(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> 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 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<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 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<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 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<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 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<JS::Object> 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<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 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<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 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");
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
141
Libraries/LibWeb/ContentSecurityPolicy/Violation.h
Normal file
141
Libraries/LibWeb/ContentSecurityPolicy/Violation.h
Normal 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 violation’s 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
|
||||
// violation’s 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;
|
||||
};
|
||||
|
||||
}
|
|
@ -103,6 +103,7 @@ namespace Web::ContentSecurityPolicy {
|
|||
class Policy;
|
||||
class PolicyList;
|
||||
class SecurityPolicyViolationEvent;
|
||||
class Violation;
|
||||
struct SecurityPolicyViolationEventInit;
|
||||
struct SerializedPolicy;
|
||||
}
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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&);
|
||||
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue