diff --git a/AGENTS.md b/AGENTS.md index f1ec225..8b2c3c3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,8 +32,11 @@ The implementation targets [W3C CSP Level 3](https://www.w3.org/TR/CSP3/). Key s ``` src/main/java/org/htmlunit/csp/ ├── Policy.java # Central class: parsing and high-level querying -├── PolicyList.java # List of policies (from comma-separated CSP headers) +├── PolicyList.java # List of policies (AND-query via CspQueries) +├── CspQueries.java # Shared enforcement query API (Policy + PolicyList) +├── CspQueriesInOrigin.java # Origin-bound sugar over CspQueries ├── PolicyInOrigin.java # Policy bound to a specific origin for querying +├── PolicyListInOrigin.java # PolicyList + origin (extends CspQueriesInOrigin) ├── Directive.java # Base class for all directive types ├── FetchDirectiveKind.java # Enum of fetch directives with fallback chains ├── Constants.java # Regex patterns, port constants diff --git a/README.md b/README.md index 87df8f8..1df5700 100644 --- a/README.md +++ b/README.md @@ -81,12 +81,47 @@ Policy policy = Policy.parseSerializedCSP(policyText, (severity, message, direct }); ``` -For a policy from a `` element, pass `true` as the third argument so that `frame-ancestors`, `sandbox`, and `report-uri` produce warnings (those directives are ignored when delivered via meta): +For a policy from a [``](https://html.spec.whatwg.org/multipage/semantics.html#attr-meta-http-equiv-content-security-policy) element, pass `true` as the third argument so that `frame-ancestors`, `sandbox`, and `report-uri` produce warnings (HTML requires that those directives must not appear in meta `content`, and browsers ignore them): ```java Policy metaPolicy = Policy.parseSerializedCSP(metaContent, consumer, true); ``` +Those three directives remain available via getters for analysis, but `allows*` methods ignore `frame-ancestors` and `sandbox` on meta-delivered policies (matching browser enforce after HTML strips them). See also [CSP3 meta element](https://w3c.github.io/webappsec-csp/#meta-element). + +### Multiple Policies + +Browsers do not conceptually merge multiple policies into a single policy. They independently evaluate each policy and allow an action only if every policy allows it ([CSP3 multiple policies](https://w3c.github.io/webappsec-csp/#multiple-policies)). Accordingly, this library preserves policies separately and evaluates queries across the collection (`PolicyList`, stopping at the first denying policy). An empty `PolicyList` is unrestricted: all `allows*` methods return `true`. + +`Policy` and `PolicyList` share the enforcement query surface via `CspQueries`. Origin-bound helpers (`PolicyInOrigin` / `PolicyListInOrigin`) extend `CspQueriesInOrigin`, so new query methods belong on the interface (and one origin wrapper), not four copy-pasted classes. + +```java +// Several Content-Security-Policy header values (each may itself be a comma-separated list) +PolicyList list = PolicyList.ofSerialized(List.of( + "img-src img.example.com", + "img-src more-images.example.com" +), Policy.PolicyListErrorConsumer.ignored); + +PolicyListInOrigin bound = new PolicyListInOrigin(list, URI.parseURI("https://example.com").orElseThrow()); + +boolean allowed = bound.allowsImageFromSource(URI.parseURI("https://img.example.com/a.png").orElseThrow()); +// allowed == false — second policy blocks even though first allows + +// Or a single comma-separated header value: +PolicyList fromOneHeader = Policy.parseSerializedCSPList( + "img-src 'self', script-src 'none'", Policy.PolicyListErrorConsumer.ignored); +``` + +Keep Report-Only policies in a separate `PolicyList`. Directive inspection and parse warnings stay per member policy via `getPolicies()`. + +`ofSerialized` always parses as header delivery (`deliveredViaMeta = false`). For mixed header + meta, parse each `Policy` with the correct flag, then build the list: + +```java +Policy headerPolicy = Policy.parseSerializedCSP(headerValue, consumer, false); +Policy metaPolicy = Policy.parseSerializedCSP(metaContent, consumer, true); +PolicyList enforce = new PolicyList(List.of(headerPolicy, metaPolicy)); +``` + ### Query a Policy The high-level querying methods allow you to specify whatever relevant information you have. The missing information will be assumed to be worst-case - that is, these methods will return `true` only if any object which matches the provided characteristics would be allowed, regardless of its other characteristics. diff --git a/src/main/java/org/htmlunit/csp/CspQueries.java b/src/main/java/org/htmlunit/csp/CspQueries.java new file mode 100644 index 0000000..7fbcbfd --- /dev/null +++ b/src/main/java/org/htmlunit/csp/CspQueries.java @@ -0,0 +1,281 @@ +/* + * Copyright (c) 2023-2026 Ronald Brill. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.htmlunit.csp; + +import java.util.Optional; + +import org.htmlunit.csp.url.URLWithScheme; +import org.htmlunit.csp.value.MediaType; + +/** + * Enforcement query API shared by {@link Policy} and {@link PolicyList}. + *
+ * For a single {@link Policy}, each method applies that policy's rules. + * For a {@link PolicyList}, each method returns {@code true} only if every + * member policy allows the resource (an empty list is unrestricted). + * Semantics of individual checks are documented on {@link Policy}. + *
+ * + * @author Ronald Brill + * @see Policy + * @see PolicyList + * @see CspQueriesInOrigin + * @since 5.4.0 + */ +public interface CspQueries { + + /** + * Returns whether an external script is allowed. + * + * @param nonce script nonce, if any + * @param integrity SRI metadata, if any + * @param scriptUrl script URL, if known + * @param parserInserted whether the script is parser-inserted + * @param origin protected-resource origin, if known + * @return {@code true} if allowed under this query target's rules + * @see Policy#allowsExternalScript + */ + boolean allowsExternalScript( + Optional+ * Use {@link PolicyInOrigin} or {@link PolicyListInOrigin} when a typed + * accessor for the underlying {@link Policy} / {@link PolicyList} is needed. + *
+ * + * @author Ronald Brill + * @see CspQueries + * @see PolicyInOrigin + * @see PolicyListInOrigin + * @since 5.4.0 + */ +public class CspQueriesInOrigin { + private final CspQueries queries_; + private final URLWithScheme origin_; + + /** + * Ctor. + * + * @param queries the query target ({@link Policy} or {@link PolicyList}) + * @param origin the origin of the protected resource + * @throws NullPointerException if {@code queries} or {@code origin} is {@code null} + */ + public CspQueriesInOrigin(final CspQueries queries, final URLWithScheme origin) { + queries_ = Objects.requireNonNull(queries, "queries"); + origin_ = Objects.requireNonNull(origin, "origin"); + } + + /** + * Returns the underlying query target. + * + * @return the {@link CspQueries} associated with this wrapper + */ + public CspQueries getQueries() { + return queries_; + } + + /** + * Returns the underlying origin. + * + * @return the origin of the protected resource + */ + public URLWithScheme getOrigin() { + return origin_; + } + + /** + * Returns whether an external script from the given URL is allowed. + * + * @param url the URL of the external script + * @return {@code true} if allowed under this query target's rules + * @see CspQueries#allowsExternalScript + */ + public boolean allowsScriptFromSource(final URLWithScheme url) { + return queries_.allowsExternalScript(Optional.empty(), + Optional.empty(), Optional.of(url), Optional.empty(), Optional.of(origin_)); + } + + /** + * Returns whether an external stylesheet from the given URL is allowed. + * + * @param url the URL of the external stylesheet + * @return {@code true} if allowed under this query target's rules + * @see CspQueries#allowsExternalStyle + */ + public boolean allowsStyleFromSource(final URLWithScheme url) { + return queries_.allowsExternalStyle(Optional.empty(), Optional.of(url), Optional.of(origin_)); + } + + /** + * Returns whether an image from the given URL is allowed. + * + * @param url the URL of the image resource + * @return {@code true} if allowed under this query target's rules + * @see CspQueries#allowsImage + */ + public boolean allowsImageFromSource(final URLWithScheme url) { + return queries_.allowsImage(Optional.of(url), Optional.of(origin_)); + } + + /** + * Returns whether a frame from the given URL is allowed. + * + * @param url the URL of the framed resource + * @return {@code true} if allowed under this query target's rules + * @see CspQueries#allowsFrame + */ + public boolean allowsFrameFromSource(final URLWithScheme url) { + return queries_.allowsFrame(Optional.of(url), Optional.of(origin_)); + } + + /** + * Returns whether a worker from the given URL is allowed. + * + * @param url the URL of the worker script + * @return {@code true} if allowed under this query target's rules + * @see CspQueries#allowsWorker + */ + public boolean allowsWorkerFromSource(final URLWithScheme url) { + return queries_.allowsWorker(Optional.of(url), Optional.of(origin_)); + } + + /** + * Returns whether a font from the given URL is allowed. + * + * @param url the URL of the font resource + * @return {@code true} if allowed under this query target's rules + * @see CspQueries#allowsFont + */ + public boolean allowsFontFromSource(final URLWithScheme url) { + return queries_.allowsFont(Optional.of(url), Optional.of(origin_)); + } + + /** + * Returns whether an object from the given URL is allowed. + * + * @param url the URL of the object resource + * @return {@code true} if allowed under this query target's rules + * @see CspQueries#allowsObject + */ + public boolean allowsObjectFromSource(final URLWithScheme url) { + return queries_.allowsObject(Optional.of(url), Optional.of(origin_)); + } + + /** + * Returns whether media from the given URL is allowed. + * + * @param url the URL of the media resource + * @return {@code true} if allowed under this query target's rules + * @see CspQueries#allowsMedia + */ + public boolean allowsMediaFromSource(final URLWithScheme url) { + return queries_.allowsMedia(Optional.of(url), Optional.of(origin_)); + } + + /** + * Returns whether an application manifest from the given URL is allowed. + * + * @param url the URL of the manifest resource + * @return {@code true} if allowed under this query target's rules + * @see CspQueries#allowsApplicationManifest + */ + public boolean allowsManifestFromSource(final URLWithScheme url) { + return queries_.allowsApplicationManifest(Optional.of(url), Optional.of(origin_)); + } + + /** + * Returns whether a prefetch from the given URL is allowed. + * + * @param url the URL to prefetch + * @return {@code true} if allowed under this query target's rules + * @see CspQueries#allowsPrefetch + */ + public boolean allowsPrefetchFromSource(final URLWithScheme url) { + return queries_.allowsPrefetch(Optional.of(url), Optional.of(origin_)); + } + + /** + * Returns whether unsafe inline script is allowed. + * + * @return {@code true} if allowed under this query target's rules + * @see CspQueries#allowsInlineScript + */ + public boolean allowsUnsafeInlineScript() { + return queries_.allowsInlineScript(Optional.empty(), Optional.empty(), Optional.empty()); + } + + /** + * Returns whether unsafe inline style is allowed. + * + * @return {@code true} if allowed under this query target's rules + * @see CspQueries#allowsInlineStyle + */ + public boolean allowsUnsafeInlineStyle() { + return queries_.allowsInlineStyle(Optional.empty(), Optional.empty()); + } + + /** + * Returns whether eval is allowed. + * + * @return {@code true} if allowed under this query target's rules + * @see CspQueries#allowsEval + */ + public boolean allowsEval() { + return queries_.allowsEval(); + } + + /** + * Returns whether a connection to the given URL is allowed. + * + * @param url the URL to connect to + * @return {@code true} if allowed under this query target's rules + * @see CspQueries#allowsConnection + */ + public boolean allowsConnection(final URLWithScheme url) { + return queries_.allowsConnection(Optional.of(url), Optional.of(origin_)); + } + + /** + * Returns whether navigation to the given URL is allowed. + * + * @param url the navigation target URL + * @return {@code true} if allowed under this query target's rules + * @see CspQueries#allowsNavigation + */ + public boolean allowsNavigation(final URLWithScheme url) { + return queries_.allowsNavigation(Optional.of(url), + Optional.empty(), Optional.empty(), Optional.of(origin_)); + } + + /** + * Returns whether the given frame ancestor is allowed. + * + * @param url the URL of the ancestor frame + * @return {@code true} if allowed under this query target's rules + * @see CspQueries#allowsFrameAncestor + */ + public boolean allowsFrameAncestor(final URLWithScheme url) { + return queries_.allowsFrameAncestor(Optional.of(url), Optional.of(origin_)); + } + + /** + * Returns whether a form action to the given URL is allowed. + * + * @param url the form action target URL + * @return {@code true} if allowed under this query target's rules + * @see CspQueries#allowsFormAction + */ + public boolean allowsFormAction(final URLWithScheme url) { + return queries_.allowsFormAction(Optional.of(url), + Optional.empty(), Optional.empty(), Optional.of(origin_)); + } +} diff --git a/src/main/java/org/htmlunit/csp/Policy.java b/src/main/java/org/htmlunit/csp/Policy.java index 389c4a3..31abc73 100644 --- a/src/main/java/org/htmlunit/csp/Policy.java +++ b/src/main/java/org/htmlunit/csp/Policy.java @@ -63,9 +63,10 @@ * CSP specification. * * + * @see CspQueries * @see W3C Content Security Policy Level 3 */ -public final class Policy { +public final class Policy implements CspQueries { // Things we don't preserve: // - Whitespace // - Empty directives or policies (as in `; ;` or `, ,`) @@ -544,6 +545,12 @@ public boolean blockAllMixedContent() { /** * Returns whether this policy was parsed as delivered via a {@code meta} element. + *+ * When {@code true}, {@code frame-ancestors}, {@code sandbox}, and {@code report-uri} + * still appear in getters for analysis, but {@code allows*} methods ignore + * {@code frame-ancestors} and {@code sandbox} (matching browser enforce after the + * HTML meta algorithm strips those directives). + *
* * @return {@code true} if {@link #parseSerializedCSP(String, PolicyErrorConsumer, boolean)} * was called with {@code deliveredViaMeta} set to {@code true} @@ -554,6 +561,24 @@ public boolean deliveredViaMeta() { return deliveredViaMeta_; } + /** + * Sandbox effective for {@code allows*} queries. + * Meta-delivered {@code sandbox} is ignored for enforce simulation + * (HTML strips it before apply) but remains available via {@link #sandbox()}. + */ + private SandboxDirective sandboxForQuery() { + return deliveredViaMeta_ ? null : sandbox_; + } + + /** + * Frame-ancestors effective for {@code allows*} queries. + * Meta-delivered {@code frame-ancestors} is ignored for enforce simulation + * but remains available via {@link #frameAncestors()}. + */ + private FrameAncestorsDirective frameAncestorsForQuery() { + return deliveredViaMeta_ ? null : frameAncestors_; + } + /** * Returns the parsed {@code form-action} directive, if present. * @@ -721,6 +746,7 @@ public Optional- * Checks the {@code frame-ancestors} directive. If not present, all ancestors are allowed. + * Checks the {@code frame-ancestors} directive. If not present, or if this policy + * was delivered via meta (where {@code frame-ancestors} is ignored), all ancestors + * are allowed. *
* * @param source the URL of the ancestor frame, if known @@ -1051,14 +1086,16 @@ public boolean allowsFrame(final Optional extends URLWithScheme> source, * @return {@code true} if this policy allows the frame ancestor * @see * frame-ancestors directive + * @see #deliveredViaMeta() */ public boolean allowsFrameAncestor(final Optional extends URLWithScheme> source, final Optional extends URLWithScheme> origin) { - if (frameAncestors_ == null) { + final FrameAncestorsDirective frameAncestors = frameAncestorsForQuery(); + if (frameAncestors == null) { return true; } return source.filter(urlWithScheme -> - doesUrlMatchSourceListInOrigin(urlWithScheme, frameAncestors_, origin)).isPresent(); + doesUrlMatchSourceListInOrigin(urlWithScheme, frameAncestors, origin)).isPresent(); } /** diff --git a/src/main/java/org/htmlunit/csp/PolicyInOrigin.java b/src/main/java/org/htmlunit/csp/PolicyInOrigin.java index 2fe2d50..a21b141 100644 --- a/src/main/java/org/htmlunit/csp/PolicyInOrigin.java +++ b/src/main/java/org/htmlunit/csp/PolicyInOrigin.java @@ -14,8 +14,6 @@ */ package org.htmlunit.csp; -import java.util.Optional; - import org.htmlunit.csp.url.URLWithScheme; /** @@ -23,16 +21,17 @@ * {@link URLWithScheme origin}, providing simplified query methods that * automatically supply the origin to the underlying policy checks. *- * Each {@code allows*} method delegates to the corresponding method on - * {@link Policy}, filling in {@code Optional.of(origin_)} for the origin - * parameter and {@code Optional.empty()} for any parameters that are not - * applicable to the simplified query (such as nonce, integrity, or - * redirect information). + * Each {@code allows*} method is defined on {@link CspQueriesInOrigin} and + * delegates to {@link Policy} ({@link CspQueries}), filling in + * {@code Optional.of(origin)} for the origin parameter and + * {@code Optional.empty()} for unused parameters (nonce, integrity, redirects). *
+ * + * @author Ronald Brill + * @see CspQueriesInOrigin + * @see PolicyListInOrigin */ -public class PolicyInOrigin { - private final Policy policy_; - private final URLWithScheme origin_; +public class PolicyInOrigin extends CspQueriesInOrigin { /** * Ctor. @@ -41,8 +40,7 @@ public class PolicyInOrigin { * @param origin the origin of the protected resource */ public PolicyInOrigin(final Policy policy, final URLWithScheme origin) { - policy_ = policy; - origin_ = origin; + super(policy, origin); } /** @@ -51,233 +49,6 @@ public PolicyInOrigin(final Policy policy, final URLWithScheme origin) { * @return the policy associated with this origin-bound wrapper */ public Policy getPolicy() { - return policy_; - } - - /** - * Returns the underlying origin. - * - * @return the policy associated with this origin-bound wrapper - */ - public URLWithScheme getOrigin() { - return origin_; - } - - // Low-level querying - - /** - * Determines whether the policy allows loading an external script from the given URL. - *- * Delegates to {@link Policy#allowsExternalScript} with no nonce, no integrity, - * unknown parser-inserted status, and this wrapper's origin. - *
- * - * @param url the URL of the external script - * @return {@code true} if the policy allows the script from the given source - */ - public boolean allowsScriptFromSource(final URLWithScheme url) { - return policy_.allowsExternalScript(Optional.empty(), - Optional.empty(), Optional.of(url), Optional.empty(), Optional.of(origin_)); - } - - /** - * Determines whether the policy allows loading an external stylesheet from the given URL. - *- * Delegates to {@link Policy#allowsExternalStyle} with no nonce and this wrapper's origin. - *
- * - * @param url the URL of the external stylesheet - * @return {@code true} if the policy allows the style from the given source - */ - public boolean allowsStyleFromSource(final URLWithScheme url) { - return policy_.allowsExternalStyle(Optional.empty(), Optional.of(url), Optional.of(origin_)); - } - - /** - * Determines whether the policy allows loading an image from the given URL. - *- * Delegates to {@link Policy#allowsImage} with this wrapper's origin. - *
- * - * @param url the URL of the image resource - * @return {@code true} if the policy allows the image from the given source - */ - public boolean allowsImageFromSource(final URLWithScheme url) { - return policy_.allowsImage(Optional.of(url), Optional.of(origin_)); - } - - /** - * Determines whether the policy allows loading a frame from the given URL. - *- * Delegates to {@link Policy#allowsFrame} with this wrapper's origin. - *
- * - * @param url the URL of the framed resource - * @return {@code true} if the policy allows the frame from the given source - */ - public boolean allowsFrameFromSource(final URLWithScheme url) { - return policy_.allowsFrame(Optional.of(url), Optional.of(origin_)); - } - - /** - * Determines whether the policy allows loading a worker from the given URL. - *- * Delegates to {@link Policy#allowsWorker} with this wrapper's origin. - *
- * - * @param url the URL of the worker script - * @return {@code true} if the policy allows the worker from the given source - */ - public boolean allowsWorkerFromSource(final URLWithScheme url) { - return policy_.allowsWorker(Optional.of(url), Optional.of(origin_)); - } - - /** - * Determines whether the policy allows loading a font from the given URL. - *- * Delegates to {@link Policy#allowsFont} with this wrapper's origin. - *
- * - * @param url the URL of the font resource - * @return {@code true} if the policy allows the font from the given source - */ - public boolean allowsFontFromSource(final URLWithScheme url) { - return policy_.allowsFont(Optional.of(url), Optional.of(origin_)); - } - - /** - * Determines whether the policy allows loading an object/embed/applet from the given URL. - *- * Delegates to {@link Policy#allowsObject} with this wrapper's origin. - *
- * - * @param url the URL of the object resource - * @return {@code true} if the policy allows the object from the given source - */ - public boolean allowsObjectFromSource(final URLWithScheme url) { - return policy_.allowsObject(Optional.of(url), Optional.of(origin_)); - } - - /** - * Determines whether the policy allows loading media (audio/video) from the given URL. - *- * Delegates to {@link Policy#allowsMedia} with this wrapper's origin. - *
- * - * @param url the URL of the media resource - * @return {@code true} if the policy allows the media from the given source - */ - public boolean allowsMediaFromSource(final URLWithScheme url) { - return policy_.allowsMedia(Optional.of(url), Optional.of(origin_)); - } - - /** - * Determines whether the policy allows loading an application manifest from the given URL. - *- * Delegates to {@link Policy#allowsApplicationManifest} with this wrapper's origin. - *
- * - * @param url the URL of the manifest resource - * @return {@code true} if the policy allows the manifest from the given source - */ - public boolean allowsManifestFromSource(final URLWithScheme url) { - return policy_.allowsApplicationManifest(Optional.of(url), Optional.of(origin_)); - } - - /** - * Determines whether the policy allows a prefetch from the given URL. - *- * Delegates to {@link Policy#allowsPrefetch} with this wrapper's origin. - *
- * - * @param url the URL to prefetch - * @return {@code true} if the policy allows the prefetch from the given source - */ - public boolean allowsPrefetchFromSource(final URLWithScheme url) { - return policy_.allowsPrefetch(Optional.of(url), Optional.of(origin_)); - } - - /** - * Determines whether the policy allows any inline script without a nonce or hash. - *- * Delegates to {@link Policy#allowsInlineScript} with no nonce, no source text, - * and unknown parser-inserted status. This effectively checks whether - * {@code 'unsafe-inline'} is active for scripts. - *
- * - * @return {@code true} if the policy allows inline scripts without specific credentials - */ - public boolean allowsUnsafeInlineScript() { - return policy_.allowsInlineScript(Optional.empty(), Optional.empty(), Optional.empty()); - } - - /** - * Determines whether the policy allows any inline style without a nonce or hash. - *- * Delegates to {@link Policy#allowsInlineStyle} with no nonce and no source text. - * This effectively checks whether {@code 'unsafe-inline'} is active for styles. - *
- * - * @return {@code true} if the policy allows inline styles without specific credentials - */ - public boolean allowsUnsafeInlineStyle() { - return policy_.allowsInlineStyle(Optional.empty(), Optional.empty()); - } - - /** - * Determines whether the policy allows a connection (e.g. {@code fetch()}, - * {@code XMLHttpRequest}, {@code WebSocket}) to the given URL. - *- * Delegates to {@link Policy#allowsConnection} with this wrapper's origin. - *
- * - * @param url the URL to connect to - * @return {@code true} if the policy allows the connection to the given source - */ - public boolean allowsConnection(final URLWithScheme url) { - return policy_.allowsConnection(Optional.of(url), Optional.of(origin_)); - } - - /** - * Determines whether the policy allows navigation to the given URL. - *- * Delegates to {@link Policy#allowsNavigation} with unknown redirect status, - * no redirect target, and this wrapper's origin. - *
- * - * @param url the navigation target URL - * @return {@code true} if the policy allows navigation to the given URL - */ - public boolean allowsNavigation(final URLWithScheme url) { - return policy_.allowsNavigation(Optional.of(url), - Optional.empty(), Optional.empty(), Optional.of(origin_)); - } - - /** - * Determines whether the policy allows being framed by the given ancestor URL. - *- * Delegates to {@link Policy#allowsFrameAncestor} with this wrapper's origin. - *
- * - * @param url the URL of the ancestor frame - * @return {@code true} if the policy allows the frame ancestor - */ - public boolean allowsFrameAncestor(final URLWithScheme url) { - return policy_.allowsFrameAncestor(Optional.of(url), Optional.of(origin_)); - } - - /** - * Determines whether the policy allows a form submission to the given URL. - *- * Delegates to {@link Policy#allowsFormAction} with unknown redirect status, - * no redirect target, and this wrapper's origin. - *
- * - * @param url the form action target URL - * @return {@code true} if the policy allows the form action to the given URL - */ - public boolean allowsFormAction(final URLWithScheme url) { - return policy_.allowsFormAction(Optional.of(url), - Optional.empty(), Optional.empty(), Optional.of(origin_)); + return (Policy) getQueries(); } } diff --git a/src/main/java/org/htmlunit/csp/PolicyList.java b/src/main/java/org/htmlunit/csp/PolicyList.java index 2e441e5..8d2b65d 100644 --- a/src/main/java/org/htmlunit/csp/PolicyList.java +++ b/src/main/java/org/htmlunit/csp/PolicyList.java @@ -14,56 +14,101 @@ */ package org.htmlunit.csp; -import java.util.ArrayList; import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Predicate; + +import org.htmlunit.csp.Policy.PolicyListErrorConsumer; +import org.htmlunit.csp.url.URLWithScheme; +import org.htmlunit.csp.value.MediaType; /** * Represents a list of Content Security Policies parsed from a comma-separated - * serialized CSP list. + * serialized CSP list, or combined from multiple header / meta deliveries. ** A serialized CSP list (e.g. the value of a {@code Content-Security-Policy} * HTTP header) may contain multiple policies separated by commas. Each policy * is parsed independently, and a resource is blocked if any policy - * in the list blocks it. + * in the list blocks it — equivalently, {@code allows*} methods return + * {@code true} only when every member policy allows the resource. + * See + * CSP3 + * multiple policies. + *
+ *+ * Implements {@link CspQueries}: each query method AND-delegates to member + * {@link Policy} instances (short-circuit on the first deny). An empty list + * is unrestricted (all {@code allows*} return {@code true}). *
** Instances are created by - * {@link Policy#parseSerializedCSPList(String, Policy.PolicyListErrorConsumer)}. + * {@link Policy#parseSerializedCSPList(String, PolicyListErrorConsumer)} + * or {@link #ofSerialized(List, PolicyListErrorConsumer)}. * Empty policies (those with no directives) are omitted during parsing. *
* - * @see Policy#parseSerializedCSPList(String, Policy.PolicyListErrorConsumer) + * @author Ronald Brill + * @see CspQueries + * @see Policy#parseSerializedCSPList(String, PolicyListErrorConsumer) */ -public class PolicyList { +public class PolicyList implements CspQueries { private final List- * The returned list is a defensive copy, so modifications to it do not - * affect the internal collection of policies. + * Each element may itself be a comma-separated CSP list (as with a single + * header field value). Values are joined with {@code ","} and parsed via + * {@link Policy#parseSerializedCSPList} (always as header delivery — + * {@code deliveredViaMeta = false}). For meta policies, parse with + * {@link Policy#parseSerializedCSP(String, Policy.PolicyErrorConsumer, boolean)} + * and pass the results to {@link PolicyList#PolicyList(List)}. Report-Only policies + * should be kept in a separate {@link PolicyList}. *
* - * @return a new {@code List} containing all policies + * @param serializedPoliciesOrLists header field values (not meta {@code content} + * strings that need {@code deliveredViaMeta = true}) + * @param policyListErrorConsumer consumer for parse errors/warnings + * @return the combined {@link PolicyList} + * @throws NullPointerException if {@code serializedPoliciesOrLists} or + * {@code policyListErrorConsumer} is {@code null}, or if any element + * of {@code serializedPoliciesOrLists} is {@code null} + * @since 5.4.0 + */ + public static PolicyList ofSerialized(final List* Individual policies are separated by {@code ", "}. Each policy is - * serialized using {@link Policy#toString()}. + * serialized using {@link Policy#toString()}. This is not an + * intersected / merged policy. *
* * @return the comma-separated serialized CSP list string @@ -81,4 +126,154 @@ public String toString() { } return out.toString(); } + + /** + * Returns {@code true} only if every policy in the list satisfies {@code check}. + * Stops at the first {@code false} (short-circuit). An empty list returns {@code true}. + */ + private boolean allAllow(final Predicate