LibWeb: Introduce Content Security Policy policies and directives

These form the basis of Content Security Policy. A policy is a
collection of directives that are parsed from either the
Content-Security-Policy(-Report-Only) HTTP header, or the `<meta>`
element.

The directives are what restrict the operations can be performed in the
current global execution context. For example, "frame-ancestors: none"
tells us to prevent the page from being loaded in an embedded context,
such as `<iframe>`.

You can see it a bit like OpenBSD's pledge() functionality, but for the
web platform: https://man.openbsd.org/pledge.2
This commit is contained in:
Luke Wilde 2024-11-25 16:17:17 +00:00 committed by Andreas Kling
commit e34a6c86b9
Notes: github-actions[bot] 2025-03-04 13:28:21 +00:00
20 changed files with 846 additions and 3 deletions

View file

@ -36,6 +36,13 @@ set(SOURCES
Clipboard/ClipboardItem.cpp
Compression/CompressionStream.cpp
Compression/DecompressionStream.cpp
ContentSecurityPolicy/Directives/Directive.cpp
ContentSecurityPolicy/Directives/DirectiveFactory.cpp
ContentSecurityPolicy/Directives/Names.cpp
ContentSecurityPolicy/Directives/SerializedDirective.cpp
ContentSecurityPolicy/Policy.cpp
ContentSecurityPolicy/PolicyList.cpp
ContentSecurityPolicy/SerializedPolicy.cpp
CredentialManagement/Credential.cpp
CredentialManagement/CredentialsContainer.cpp
CredentialManagement/FederatedCredential.cpp

View file

@ -0,0 +1,35 @@
/*
* Copyright (c) 2025, Luke Wilde <luke@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Runtime/Realm.h>
#include <LibWeb/ContentSecurityPolicy/Directives/Directive.h>
#include <LibWeb/ContentSecurityPolicy/Directives/DirectiveFactory.h>
#include <LibWeb/ContentSecurityPolicy/Directives/SerializedDirective.h>
namespace Web::ContentSecurityPolicy::Directives {
GC_DEFINE_ALLOCATOR(Directive);
Directive::Directive(String name, Vector<String> value)
: m_name(move(name))
, m_value(move(value))
{
}
GC::Ref<Directive> Directive::clone(JS::Realm& realm) const
{
return create_directive(realm, m_name, m_value);
}
SerializedDirective Directive::serialize() const
{
return SerializedDirective {
.name = m_name,
.value = m_value,
};
}
}

View file

@ -0,0 +1,112 @@
/*
* Copyright (c) 2025, Luke Wilde <luke@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/FlyString.h>
#include <LibGC/CellAllocator.h>
#include <LibGC/Ptr.h>
#include <LibJS/Heap/Cell.h>
#include <LibWeb/Forward.h>
namespace Web::ContentSecurityPolicy::Directives {
// https://w3c.github.io/webappsec-csp/#directives
// Each policy contains an ordered set of directives (its directive set), each of which controls a specific behavior.
// The directives defined in this document are described in detail in § 6 Content Security Policy Directives.
class Directive : public JS::Cell {
GC_CELL(Directive, JS::Cell)
GC_DECLARE_ALLOCATOR(Directive);
public:
enum class Result {
Blocked,
Allowed,
};
enum class NavigationType {
FormSubmission,
Other,
};
enum class CheckType {
Source,
Response,
};
enum class InlineType {
Navigation,
Script,
ScriptAttribute,
Style,
StyleAttribute,
};
virtual ~Directive() = default;
// Directives have a number of associated algorithms:
// https://w3c.github.io/webappsec-csp/#directive-pre-request-check
// 1. A pre-request check, which takes a request and a policy as an argument, and is executed during
// § 4.1.2 Should request be blocked by Content Security Policy?. This algorithm returns "Allowed"
// unless otherwise specified.
[[nodiscard]] virtual Result pre_request_check(JS::Realm&, GC::Ref<Fetch::Infrastructure::Request const>, GC::Ref<Policy const>) const { return Result::Allowed; }
// https://w3c.github.io/webappsec-csp/#directive-post-request-check
// 2. A post-request check, which takes a request, a response, and a policy as arguments, and is executed during
// § 4.1.3 Should response to request be blocked by Content Security Policy?. This algorithm returns "Allowed"
// unless otherwise specified.
[[nodiscard]] virtual Result post_request_check(JS::Realm&, GC::Ref<Fetch::Infrastructure::Request const>, GC::Ref<Fetch::Infrastructure::Response const>, GC::Ref<Policy const>) const { return Result::Allowed; }
// https://w3c.github.io/webappsec-csp/#directive-inline-check
// 3. An inline check, which takes an Element, a type string, a policy, and a source string as arguments, and is
// executed during § 4.2.3 Should elements inline type behavior be blocked by Content Security Policy? and
// during § 4.2.4 Should navigation request of type be blocked by Content Security Policy? for javascript:
// requests. This algorithm returns "Allowed" unless otherwise specified.
[[nodiscard]] virtual Result inline_check(JS::Realm&, GC::Ptr<DOM::Element const>, InlineType, GC::Ref<Policy const>, String const&) const { return Result::Allowed; }
// https://w3c.github.io/webappsec-csp/#directive-initialization
// 4. An initialization, which takes a Document or global object and a policy as arguments. This algorithm is
// executed during § 4.2.1 Run CSP initialization for a Document and § 4.2.6 Run CSP initialization for
// a global object. Unless otherwise specified, it has no effect and it returns "Allowed".
[[nodiscard]] virtual Result initialization(Variant<GC::Ref<DOM::Document const>, GC::Ref<HTML::WorkerGlobalScope const>>, GC::Ref<Policy const>) const { return Result::Allowed; }
// https://w3c.github.io/webappsec-csp/#directive-pre-navigation-check
// 5. A pre-navigation check, which takes a request, a navigation type string ("form-submission" or "other")
// and a policy as arguments, and is executed during § 4.2.4 Should navigation request of type be blocked by
// Content Security Policy?. It returns "Allowed" unless otherwise specified.
[[nodiscard]] virtual Result pre_navigation_check(GC::Ref<Fetch::Infrastructure::Request const>, NavigationType, GC::Ref<Policy const>) const { return Result::Allowed; }
// https://w3c.github.io/webappsec-csp/#directive-navigation-response-check
// 6. A navigation response check, which takes a request, a navigation type string ("form-submission" or "other"),
// a response, a navigable, a check type string ("source" or "response"), and a policy as arguments, and is
// executed during § 4.2.5 Should navigation response to navigation request of type in target be blocked by
// Content Security Policy?. It returns "Allowed" unless otherwise specified.
[[nodiscard]] virtual Result navigation_response_check(GC::Ref<Fetch::Infrastructure::Request const>, NavigationType, GC::Ref<Fetch::Infrastructure::Response const>, GC::Ref<HTML::Navigable const>, CheckType, GC::Ref<Policy const>) const { return Result::Allowed; }
// https://w3c.github.io/webappsec-csp/#directive-webrtc-pre-connect-check
// 7. A webrtc pre-connect check, which takes a policy, and is executed during § 4.3.1 Should RTC connections be
// blocked for global?. It returns "Allowed" unless otherwise specified.
[[nodiscard]] virtual Result webrtc_pre_connect_check(GC::Ref<Policy const>) const { return Result::Allowed; }
[[nodiscard]] String const& name() const { return m_name; }
[[nodiscard]] Vector<String> const& value() const { return m_value; }
[[nodiscard]] GC::Ref<Directive> clone(JS::Realm&) const;
[[nodiscard]] SerializedDirective serialize() const;
protected:
Directive(String name, Vector<String> value);
private:
// https://w3c.github.io/webappsec-csp/#directive-name
// https://w3c.github.io/webappsec-csp/#directive-value
// Each directive is a name / value pair. The name is a non-empty string, and the value is a set of non-empty strings.
// The value MAY be empty.
String m_name;
Vector<String> m_value;
};
}

View file

@ -0,0 +1,19 @@
/*
* Copyright (c) 2025, Luke Wilde <luke@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Runtime/Realm.h>
#include <LibWeb/ContentSecurityPolicy/Directives/Directive.h>
#include <LibWeb/ContentSecurityPolicy/Directives/DirectiveFactory.h>
namespace Web::ContentSecurityPolicy::Directives {
GC::Ref<Directive> create_directive(JS::Realm& realm, String name, Vector<String> value)
{
dbgln("Potential FIXME: Creating unknown Content Security Policy directive: {}", name);
return realm.create<Directive>(move(name), move(value));
}
}

View file

@ -0,0 +1,18 @@
/*
* Copyright (c) 2025, Luke Wilde <luke@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/String.h>
#include <AK/Vector.h>
#include <LibGC/Ptr.h>
#include <LibWeb/Forward.h>
namespace Web::ContentSecurityPolicy::Directives {
[[nodiscard]] GC::Ref<Directive> create_directive(JS::Realm&, String name, Vector<String> value);
}

View file

@ -0,0 +1,16 @@
/*
* Copyright (c) 2025, Luke Wilde <luke@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/ContentSecurityPolicy/Directives/Names.h>
namespace Web::ContentSecurityPolicy::Directives::Names {
#define __ENUMERATE_DIRECTIVE_NAME(name, value) \
FlyString name = value##_fly_string;
ENUMERATE_DIRECTIVE_NAMES
#undef __ENUMERATE_DIRECTIVE_NAME
}

View file

@ -0,0 +1,44 @@
/*
* Copyright (c) 2025, Luke Wilde <luke@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/FlyString.h>
namespace Web::ContentSecurityPolicy::Directives::Names {
#define ENUMERATE_DIRECTIVE_NAMES \
__ENUMERATE_DIRECTIVE_NAME(BaseUri, "base-uri") \
__ENUMERATE_DIRECTIVE_NAME(ChildSrc, "child-src") \
__ENUMERATE_DIRECTIVE_NAME(ConnectSrc, "connect-src") \
__ENUMERATE_DIRECTIVE_NAME(DefaultSrc, "default-src") \
__ENUMERATE_DIRECTIVE_NAME(FontSrc, "font-src") \
__ENUMERATE_DIRECTIVE_NAME(FormAction, "form-action") \
__ENUMERATE_DIRECTIVE_NAME(FrameAncestors, "frame-ancestors") \
__ENUMERATE_DIRECTIVE_NAME(FrameSrc, "frame-src") \
__ENUMERATE_DIRECTIVE_NAME(ImgSrc, "img-src") \
__ENUMERATE_DIRECTIVE_NAME(ManifestSrc, "manifest-src") \
__ENUMERATE_DIRECTIVE_NAME(MediaSrc, "media-src") \
__ENUMERATE_DIRECTIVE_NAME(ObjectSrc, "object-src") \
__ENUMERATE_DIRECTIVE_NAME(ReportTo, "report-to") \
__ENUMERATE_DIRECTIVE_NAME(ReportUri, "report-uri") \
__ENUMERATE_DIRECTIVE_NAME(Sandbox, "sandbox") \
__ENUMERATE_DIRECTIVE_NAME(ScriptSrc, "script-src") \
__ENUMERATE_DIRECTIVE_NAME(ScriptSrcElem, "script-src-elem") \
__ENUMERATE_DIRECTIVE_NAME(ScriptSrcAttr, "script-src-attr") \
__ENUMERATE_DIRECTIVE_NAME(StyleSrc, "style-src") \
__ENUMERATE_DIRECTIVE_NAME(StyleSrcElem, "style-src-elem") \
__ENUMERATE_DIRECTIVE_NAME(StyleSrcAttr, "style-src-attr") \
__ENUMERATE_DIRECTIVE_NAME(WebRTC, "webrtc") \
__ENUMERATE_DIRECTIVE_NAME(WorkerSrc, "worker-src")
#define __ENUMERATE_DIRECTIVE_NAME(name, value) extern FlyString name;
ENUMERATE_DIRECTIVE_NAMES
#undef __ENUMERATE_DIRECTIVE_NAME
void initialize_strings();
}

View file

@ -0,0 +1,33 @@
/*
* Copyright (c) 2025, Luke Wilde <luke@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibIPC/Decoder.h>
#include <LibIPC/Encoder.h>
#include <LibWeb/ContentSecurityPolicy/Directives/SerializedDirective.h>
namespace IPC {
template<>
ErrorOr<void> encode(Encoder& encoder, Web::ContentSecurityPolicy::Directives::SerializedDirective const& serialized_directive)
{
TRY(encoder.encode(serialized_directive.name));
TRY(encoder.encode(serialized_directive.value));
return {};
}
template<>
ErrorOr<Web::ContentSecurityPolicy::Directives::SerializedDirective> decode(Decoder& decoder)
{
Web::ContentSecurityPolicy::Directives::SerializedDirective serialized_directive {};
serialized_directive.name = TRY(decoder.decode<String>());
serialized_directive.value = TRY(decoder.decode<Vector<String>>());
return serialized_directive;
}
}

View file

@ -0,0 +1,30 @@
/*
* Copyright (c) 2025, Luke Wilde <luke@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/String.h>
#include <AK/Vector.h>
#include <LibIPC/Forward.h>
namespace Web::ContentSecurityPolicy::Directives {
struct SerializedDirective {
String name;
Vector<String> value;
};
}
namespace IPC {
template<>
ErrorOr<void> encode(Encoder&, Web::ContentSecurityPolicy::Directives::SerializedDirective const&);
template<>
ErrorOr<Web::ContentSecurityPolicy::Directives::SerializedDirective> decode(Decoder&);
}

View file

@ -0,0 +1,199 @@
/*
* Copyright (c) 2025, Luke Wilde <luke@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/GenericLexer.h>
#include <AK/String.h>
#include <LibWeb/ContentSecurityPolicy/Directives/DirectiveFactory.h>
#include <LibWeb/ContentSecurityPolicy/Directives/SerializedDirective.h>
#include <LibWeb/ContentSecurityPolicy/Policy.h>
#include <LibWeb/ContentSecurityPolicy/PolicyList.h>
#include <LibWeb/ContentSecurityPolicy/SerializedPolicy.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Headers.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Responses.h>
#include <LibWeb/Infra/CharacterTypes.h>
#include <LibWeb/Infra/Strings.h>
namespace Web::ContentSecurityPolicy {
GC_DEFINE_ALLOCATOR(Policy);
// https://w3c.github.io/webappsec-csp/#abstract-opdef-parse-a-serialized-csp
GC::Ref<Policy> Policy::parse_a_serialized_csp(JS::Realm& realm, Variant<ByteBuffer, String> serialized, Source source, Disposition disposition)
{
// To parse a serialized CSP, given a byte sequence or string serialized, a source source, and a disposition disposition,
// execute the following steps.
// This algorithm returns a Content Security Policy object. If serialized could not be parsed, the objects directive
// set will be empty.
// 1. If serialized is a byte sequence, then set serialized to be the result of isomorphic decoding serialized.
auto serialized_string = serialized.has<String>()
? serialized.get<String>()
: Infra::isomorphic_decode(serialized.get<ByteBuffer>());
// 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_source = source;
policy->m_disposition = disposition;
// 3. For each token returned by strictly splitting serialized on the U+003B SEMICOLON character (;):
auto tokens = MUST(serialized_string.split(';', SplitBehavior::KeepEmpty));
for (auto token : tokens) {
// 1. Strip leading and trailing ASCII whitespace from token.
auto stripped_token = MUST(token.trim(Infra::ASCII_WHITESPACE));
auto stripped_token_view = stripped_token.bytes_as_string_view();
// 2. If token is an empty string, or if token is not an ASCII string, continue.
if (stripped_token.is_empty() || !all_of(stripped_token_view, is_ascii))
continue;
// 3. Let directive name be the result of collecting a sequence of code points from token which are not
// ASCII whitespace.
GenericLexer lexer(stripped_token_view);
auto directive_name = lexer.consume_until(Infra::is_ascii_whitespace);
// 4. Set directive name to be the result of running ASCII lowercase on directive name.
// Spec Note: Directive names are case-insensitive, that is: script-SRC 'none' and ScRiPt-sRc 'none' are
// equivalent.
auto lowercase_directive_name = MUST(Infra::to_ascii_lowercase(directive_name));
// 5. If policys directive set contains a directive whose name is directive name, continue.
if (policy->contains_directive_with_name(lowercase_directive_name)) {
// Spec Note: In this case, the user agent SHOULD notify developers that a duplicate directive was
// ignored. A console warning might be appropriate, for example.
dbgln("Ignoring duplicate Content Security Policy directive: {}", lowercase_directive_name);
continue;
}
// 6. Let directive value be the result of splitting token on ASCII whitespace.
auto rest_of_the_token = lexer.consume_all();
auto directive_value_views = rest_of_the_token.split_view_if(Infra::is_ascii_whitespace);
Vector<String> directive_value;
for (auto directive_value_view : directive_value_views) {
String directive_value_entry = MUST(String::from_utf8(directive_value_view));
directive_value.append(move(directive_value_entry));
}
// 7. Let directive be a new directive whose name is directive name, and value is directive value.
auto directive = Directives::create_directive(realm, move(lowercase_directive_name), move(directive_value));
// 8. Append directive to policys directive set.
policy->m_directives.append(directive);
}
// 4. Return policy.
return policy;
}
// https://w3c.github.io/webappsec-csp/#abstract-opdef-parse-a-responses-content-security-policies
GC::Ref<PolicyList> Policy::parse_a_responses_content_security_policies(JS::Realm& realm, GC::Ref<Fetch::Infrastructure::Response const> response)
{
// To parse a responses Content Security Policies given a response response, execute the following steps.
// This algorithm returns a list of Content Security Policy objects. If the policies cannot be parsed,
// the returned list will be empty.
// 1. Let policies be an empty list.
GC::RootVector<GC::Ref<Policy>> policies(realm.heap());
// 2. For each token returned by extracting header list values given Content-Security-Policy and responses header
// list:
auto enforce_policy_tokens_or_failure = Fetch::Infrastructure::extract_header_list_values("Content-Security-Policy"sv.bytes(), response->header_list());
auto enforce_policy_tokens = enforce_policy_tokens_or_failure.has<Vector<ByteBuffer>>() ? enforce_policy_tokens_or_failure.get<Vector<ByteBuffer>>() : Vector<ByteBuffer> {};
for (auto enforce_policy_token : enforce_policy_tokens) {
// 1. Let policy be the result of parsing token, with a source of "header", and a disposition of "enforce".
auto policy = parse_a_serialized_csp(realm, enforce_policy_token, Policy::Source::Header, Policy::Disposition::Enforce);
// 2. If policys directive set is not empty, append policy to policies.
if (!policy->m_directives.is_empty()) {
policies.append(policy);
}
}
// 3. For each token returned by extracting header list values given Content-Security-Policy-Report-Only and
// responses header list:
auto report_policy_tokens_or_failure = Fetch::Infrastructure::extract_header_list_values("Content-Security-Policy-Report-Only"sv.bytes(), response->header_list());
auto report_policy_tokens = report_policy_tokens_or_failure.has<Vector<ByteBuffer>>() ? report_policy_tokens_or_failure.get<Vector<ByteBuffer>>() : Vector<ByteBuffer> {};
for (auto report_policy_token : report_policy_tokens) {
// 1. Let policy be the result of parsing token, with a source of "header", and a disposition of "report".
auto policy = parse_a_serialized_csp(realm, report_policy_token, Policy::Source::Header, Policy::Disposition::Report);
// 2. If policys directive set is not empty, append policy to policies.
if (!policy->m_directives.is_empty()) {
policies.append(policy);
}
}
// 4. For each policy of policies:
for (auto& policy : policies) {
// 1. Set policys self-origin to responses url's origin.
policy->m_self_origin = response->url()->origin();
}
// 5. Return policies.
return PolicyList::create(realm, policies);
}
GC::Ref<Policy> Policy::create_from_serialized_policy(JS::Realm& realm, SerializedPolicy const& serialized_policy)
{
auto policy = realm.create<Policy>();
for (auto const& serialized_directive : serialized_policy.directives) {
auto directive = Directives::create_directive(realm, serialized_directive.name, serialized_directive.value);
policy->m_directives.append(directive);
}
policy->m_disposition = serialized_policy.disposition;
policy->m_source = serialized_policy.source;
policy->m_self_origin = serialized_policy.self_origin;
return policy;
}
bool Policy::contains_directive_with_name(StringView name) const
{
auto maybe_directive = m_directives.find_if([name](auto const& directive) {
return directive->name() == name;
});
return !maybe_directive.is_end();
}
GC::Ref<Policy> Policy::clone(JS::Realm& realm) const
{
auto policy = realm.create<Policy>();
for (auto directive : m_directives) {
auto cloned_directive = directive->clone(realm);
policy->m_directives.append(cloned_directive);
}
policy->m_disposition = m_disposition;
policy->m_source = m_source;
policy->m_self_origin = m_self_origin;
return policy;
}
SerializedPolicy Policy::serialize() const
{
Vector<Directives::SerializedDirective> serialized_directives;
for (auto directive : m_directives) {
serialized_directives.append(directive->serialize());
}
return SerializedPolicy {
.directives = move(serialized_directives),
.disposition = m_disposition,
.source = m_source,
.self_origin = m_self_origin,
};
}
void Policy::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_directives);
}
}

View file

@ -0,0 +1,82 @@
/*
* Copyright (c) 2025, Luke Wilde <luke@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibGC/Ptr.h>
#include <LibURL/Origin.h>
#include <LibWeb/ContentSecurityPolicy/Directives/Directive.h>
#include <LibWeb/Forward.h>
namespace Web::ContentSecurityPolicy {
#define ENUMERATE_DISPOSITION_TYPES \
__ENUMERATE_DISPOSITION_TYPE(Enforce, "enforce") \
__ENUMERATE_DISPOSITION_TYPE(Report, "report")
// https://w3c.github.io/webappsec-csp/#content-security-policy-object
// A policy defines allowed and restricted behaviors, and may be applied to a Document, WorkerGlobalScope,
// or WorkletGlobalScope.
class Policy final : public JS::Cell {
GC_CELL(Policy, JS::Cell);
GC_DECLARE_ALLOCATOR(Policy);
public:
enum class Disposition {
#define __ENUMERATE_DISPOSITION_TYPE(type, _) type,
ENUMERATE_DISPOSITION_TYPES
#undef __ENUMERATE_DISPOSITION_TYPE
};
enum class Source {
Header,
Meta,
};
~Policy() = default;
[[nodiscard]] static GC::Ref<Policy> parse_a_serialized_csp(JS::Realm&, Variant<ByteBuffer, String> serialized, Source source, Disposition disposition);
[[nodiscard]] static GC::Ref<PolicyList> parse_a_responses_content_security_policies(JS::Realm&, GC::Ref<Fetch::Infrastructure::Response const> response);
[[nodiscard]] static GC::Ref<Policy> create_from_serialized_policy(JS::Realm&, SerializedPolicy const&);
[[nodiscard]] Vector<GC::Ref<Directives::Directive>> const& directives() const { return m_directives; }
[[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]] bool contains_directive_with_name(StringView name) const;
[[nodiscard]] GC::Ref<Policy> clone(JS::Realm&) const;
[[nodiscard]] SerializedPolicy serialize() const;
protected:
virtual void visit_edges(Cell::Visitor&) override;
private:
Policy() = default;
// https://w3c.github.io/webappsec-csp/#policy-directive-set
// Each policy has an associated directive set, which is an ordered set of directives that define the policys
// implications when applied.
Vector<GC::Ref<Directives::Directive>> m_directives;
// https://w3c.github.io/webappsec-csp/#policy-disposition
// Each policy has an associated disposition, which is either "enforce" or "report".
Disposition m_disposition { Disposition::Enforce };
// https://w3c.github.io/webappsec-csp/#policy-source
// Each policy has an associated source, which is either "header" or "meta".
Source m_source { Source::Header };
// https://w3c.github.io/webappsec-csp/#policy-self-origin
// Each policy has an associated self-origin, which is an origin that is used when matching the 'self' keyword.
// Spec Note: This is needed to facilitate the 'self' checks of local scheme documents/workers that have inherited
// 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;
};
}

View file

@ -0,0 +1,104 @@
/*
* Copyright (c) 2025, Luke Wilde <luke@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibGC/RootVector.h>
#include <LibJS/Runtime/Realm.h>
#include <LibWeb/ContentSecurityPolicy/PolicyList.h>
#include <LibWeb/ContentSecurityPolicy/SerializedPolicy.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/HTML/Scripting/Environments.h>
#include <LibWeb/HTML/ShadowRealmGlobalScope.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/HTML/WorkerGlobalScope.h>
namespace Web::ContentSecurityPolicy {
GC_DEFINE_ALLOCATOR(PolicyList);
GC::Ref<PolicyList> PolicyList::create(JS::Realm& realm, GC::RootVector<GC::Ref<Policy>> const& policies)
{
auto policy_list = realm.create<PolicyList>();
for (auto policy : policies)
policy_list->m_policies.append(policy);
return policy_list;
}
GC::Ref<PolicyList> PolicyList::create(JS::Realm& realm, Vector<SerializedPolicy> const& serialized_policies)
{
auto policy_list = realm.create<PolicyList>();
for (auto const& serialized_policy : serialized_policies) {
auto policy = Policy::create_from_serialized_policy(realm, serialized_policy);
policy_list->m_policies.append(policy);
}
return policy_list;
}
// https://w3c.github.io/webappsec-csp/#get-csp-of-object
GC::Ptr<PolicyList> PolicyList::from_object(JS::Object& object)
{
// 1. If object is a Document return objects policy container's CSP list.
if (is<DOM::Document>(object)) {
auto& document = static_cast<DOM::Document&>(object);
return document.policy_container()->csp_list;
}
// 2. If object is a Window or a WorkerGlobalScope or a WorkletGlobalScope, return environment settings objects
// policy container's CSP list.
// FIXME: File a spec issue to make this look at ShadowRealmGlobalScope to support ShadowRealm.
if (is<HTML::Window>(object) || is<HTML::WorkerGlobalScope>(object) || is<HTML::ShadowRealmGlobalScope>(object)) {
auto& settings = HTML::relevant_principal_settings_object(object);
return settings.policy_container()->csp_list;
}
// 3. Return null.
return nullptr;
}
void PolicyList::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_policies);
}
// https://w3c.github.io/webappsec-csp/#contains-a-header-delivered-content-security-policy
bool PolicyList::contains_header_delivered_policy() const
{
// A CSP list contains a header-delivered Content Security Policy if it contains a policy whose source is "header".
auto header_delivered_entry = m_policies.find_if([](auto const& policy) {
return policy->source() == Policy::Source::Header;
});
return !header_delivered_entry.is_end();
}
HTML::SandboxingFlagSet PolicyList::csp_derived_sandboxing_flags() const
{
dbgln("FIXME: Implement PolicyList::csp_derived_sandboxing_flags");
return HTML::SandboxingFlagSet {};
}
GC::Ref<PolicyList> PolicyList::clone(JS::Realm& realm) const
{
auto policy_list = realm.create<PolicyList>();
for (auto policy : m_policies) {
auto cloned_policy = policy->clone(realm);
policy_list->m_policies.append(cloned_policy);
}
return policy_list;
}
Vector<SerializedPolicy> PolicyList::serialize() const
{
Vector<SerializedPolicy> serialized_policies;
for (auto policy : m_policies) {
serialized_policies.append(policy->serialize());
}
return serialized_policies;
}
}

View file

@ -0,0 +1,44 @@
/*
* 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 <LibWeb/ContentSecurityPolicy/Policy.h>
namespace Web::ContentSecurityPolicy {
class PolicyList final : public JS::Cell {
GC_CELL(PolicyList, JS::Cell);
GC_DECLARE_ALLOCATOR(PolicyList);
public:
[[nodiscard]] static GC::Ref<PolicyList> create(JS::Realm&, GC::RootVector<GC::Ref<Policy>> const&);
[[nodiscard]] static GC::Ref<PolicyList> create(JS::Realm&, Vector<SerializedPolicy> const&);
[[nodiscard]] static GC::Ptr<PolicyList> from_object(JS::Object&);
virtual ~PolicyList() = default;
[[nodiscard]] Vector<GC::Ref<Policy>> const& policies() const { return m_policies; }
[[nodiscard]] bool contains_header_delivered_policy() const;
[[nodiscard]] HTML::SandboxingFlagSet csp_derived_sandboxing_flags() const;
[[nodiscard]] GC::Ref<PolicyList> clone(JS::Realm&) const;
[[nodiscard]] Vector<SerializedPolicy> serialize() const;
protected:
virtual void visit_edges(Cell::Visitor&) override;
private:
PolicyList() = default;
Vector<GC::Ref<Policy>> m_policies;
};
}

View file

@ -0,0 +1,37 @@
/*
* Copyright (c) 2025, Luke Wilde <luke@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibIPC/Decoder.h>
#include <LibIPC/Encoder.h>
#include <LibWeb/ContentSecurityPolicy/SerializedPolicy.h>
namespace IPC {
template<>
ErrorOr<void> encode(Encoder& encoder, Web::ContentSecurityPolicy::SerializedPolicy const& serialized_policy)
{
TRY(encoder.encode(serialized_policy.directives));
TRY(encoder.encode(serialized_policy.disposition));
TRY(encoder.encode(serialized_policy.source));
TRY(encoder.encode(serialized_policy.self_origin));
return {};
}
template<>
ErrorOr<Web::ContentSecurityPolicy::SerializedPolicy> decode(Decoder& decoder)
{
Web::ContentSecurityPolicy::SerializedPolicy serialized_policy {};
serialized_policy.directives = TRY(decoder.decode<Vector<Web::ContentSecurityPolicy::Directives::SerializedDirective>>());
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>());
return serialized_policy;
}
}

View file

@ -0,0 +1,32 @@
/*
* Copyright (c) 2025, Luke Wilde <luke@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibIPC/Forward.h>
#include <LibWeb/ContentSecurityPolicy/Directives/SerializedDirective.h>
#include <LibWeb/ContentSecurityPolicy/Policy.h>
namespace Web::ContentSecurityPolicy {
struct SerializedPolicy {
Vector<Directives::SerializedDirective> directives;
Policy::Disposition disposition;
Policy::Source source;
URL::Origin self_origin;
};
}
namespace IPC {
template<>
ErrorOr<void> encode(Encoder&, Web::ContentSecurityPolicy::SerializedPolicy const&);
template<>
ErrorOr<Web::ContentSecurityPolicy::SerializedPolicy> decode(Decoder&);
}

View file

@ -98,6 +98,17 @@ class CompressionStream;
class DecompressionStream;
}
namespace Web::ContentSecurityPolicy {
class Policy;
class PolicyList;
struct SerializedPolicy;
}
namespace Web::ContentSecurityPolicy::Directives {
class Directive;
struct SerializedDirective;
}
namespace Web::Cookie {
struct Cookie;
struct ParsedCookie;

View file

@ -7,6 +7,8 @@
#include <LibJS/Runtime/Realm.h>
#include <LibURL/URL.h>
#include <LibWeb/ContentSecurityPolicy/Policy.h>
#include <LibWeb/ContentSecurityPolicy/PolicyList.h>
#include <LibWeb/Fetch/Infrastructure/URL.h>
#include <LibWeb/HTML/PolicyContainers.h>
#include <LibWeb/HTML/SerializedPolicyContainer.h>
@ -15,7 +17,8 @@ namespace Web::HTML {
GC_DEFINE_ALLOCATOR(PolicyContainer);
PolicyContainer::PolicyContainer(JS::Realm&)
PolicyContainer::PolicyContainer(JS::Realm& realm)
: csp_list(realm.create<ContentSecurityPolicy::PolicyList>())
{
}
@ -34,6 +37,7 @@ bool url_requires_storing_the_policy_container_in_history(URL::URL const& url)
GC::Ref<PolicyContainer> create_a_policy_container_from_serialized_policy_container(JS::Realm& realm, SerializedPolicyContainer const& serialized_policy_container)
{
GC::Ref<PolicyContainer> result = realm.create<PolicyContainer>(realm);
result->csp_list = ContentSecurityPolicy::PolicyList::create(realm, serialized_policy_container.csp_list);
result->embedder_policy = serialized_policy_container.embedder_policy;
result->referrer_policy = serialized_policy_container.referrer_policy;
return result;
@ -45,7 +49,8 @@ GC::Ref<PolicyContainer> PolicyContainer::clone(JS::Realm& realm) const
// 1. Let clone be a new policy container.
auto clone = realm.create<PolicyContainer>(realm);
// FIXME: 2. For each policy in policyContainer's CSP list, append a copy of policy into clone's CSP list.
// 2. For each policy in policyContainer's CSP list, append a copy of policy into clone's CSP list.
clone->csp_list = csp_list->clone(realm);
// 3. Set clone's embedder policy to a copy of policyContainer's embedder policy.
// NOTE: This is a C++ copy.
@ -61,9 +66,16 @@ GC::Ref<PolicyContainer> PolicyContainer::clone(JS::Realm& realm) const
SerializedPolicyContainer PolicyContainer::serialize() const
{
return SerializedPolicyContainer {
.csp_list = csp_list->serialize(),
.embedder_policy = embedder_policy,
.referrer_policy = referrer_policy,
};
}
void PolicyContainer::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(csp_list);
}
}

View file

@ -26,7 +26,8 @@ public:
virtual ~PolicyContainer() = default;
// https://html.spec.whatwg.org/multipage/origin.html#policy-container-csp-list
// FIXME: A CSP list, which is a CSP list. It is initially empty.
// A CSP list, which is a CSP list. It is initially empty.
GC::Ref<ContentSecurityPolicy::PolicyList> csp_list;
// https://html.spec.whatwg.org/multipage/origin.html#policy-container-embedder-policy
// An embedder policy, which is an embedder policy. It is initially a new embedder policy.
@ -39,6 +40,9 @@ public:
[[nodiscard]] GC::Ref<PolicyContainer> clone(JS::Realm&) const;
[[nodiscard]] SerializedPolicyContainer serialize() const;
protected:
virtual void visit_edges(Cell::Visitor&) override;
private:
PolicyContainer(JS::Realm&);
};

View file

@ -13,6 +13,7 @@ namespace IPC {
template<>
ErrorOr<void> encode(Encoder& encoder, Web::HTML::SerializedPolicyContainer const& serialized_policy_container)
{
TRY(encoder.encode(serialized_policy_container.csp_list));
TRY(encoder.encode(serialized_policy_container.embedder_policy));
TRY(encoder.encode(serialized_policy_container.referrer_policy));
@ -24,6 +25,7 @@ ErrorOr<Web::HTML::SerializedPolicyContainer> decode(Decoder& decoder)
{
Web::HTML::SerializedPolicyContainer serialized_policy_container {};
serialized_policy_container.csp_list = TRY(decoder.decode<Vector<Web::ContentSecurityPolicy::SerializedPolicy>>());
serialized_policy_container.embedder_policy = TRY(decoder.decode<Web::HTML::EmbedderPolicy>());
serialized_policy_container.referrer_policy = TRY(decoder.decode<Web::ReferrerPolicy::ReferrerPolicy>());

View file

@ -6,12 +6,14 @@
#pragma once
#include <LibWeb/ContentSecurityPolicy/SerializedPolicy.h>
#include <LibWeb/HTML/EmbedderPolicy.h>
#include <LibWeb/ReferrerPolicy/ReferrerPolicy.h>
namespace Web::HTML {
struct SerializedPolicyContainer {
Vector<ContentSecurityPolicy::SerializedPolicy> csp_list;
EmbedderPolicy embedder_policy;
ReferrerPolicy::ReferrerPolicy referrer_policy;
};