From 9067ad883ad664b835380a28643092ca6e91ccc9 Mon Sep 17 00:00:00 2001
From: kingthorin
Date: Thu, 23 Jul 2026 09:12:06 -0400
Subject: [PATCH 1/2] feat: add multi-policy AND query APIs
Browsers allow a resource only if every CSP allows it. Query that
via PolicyList / PolicyListInOrigin instead of merging to one string.
Signed-off-by: kingthorin
---
README.md | 35 +-
src/main/java/org/htmlunit/csp/Policy.java | 50 +-
.../java/org/htmlunit/csp/PolicyInOrigin.java | 11 +
.../java/org/htmlunit/csp/PolicyList.java | 352 +++++++++-
.../org/htmlunit/csp/PolicyListInOrigin.java | 238 +++++++
.../htmlunit/csp/PolicyListQueryingTest.java | 619 ++++++++++++++++++
.../java/org/htmlunit/csp/QueryingTest.java | 33 +
7 files changed, 1318 insertions(+), 20 deletions(-)
create mode 100644 src/main/java/org/htmlunit/csp/PolicyListInOrigin.java
create mode 100644 src/test/java/org/htmlunit/csp/PolicyListQueryingTest.java
diff --git a/README.md b/README.md
index 87df8f8..9e6de6a 100644
--- a/README.md
+++ b/README.md
@@ -81,12 +81,45 @@ 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 enforce every CSP delivered via headers and/or `` together: a resource is allowed only if **every** policy allows it ([CSP3 multiple policies](https://w3c.github.io/webappsec-csp/#multiple-policies)). This library does not merge policies into one string; it ANDs query results on `PolicyList`.
+
+```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/Policy.java b/src/main/java/org/htmlunit/csp/Policy.java
index 389c4a3..a728cb6 100644
--- a/src/main/java/org/htmlunit/csp/Policy.java
+++ b/src/main/java/org/htmlunit/csp/Policy.java
@@ -544,6 +544,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 +560,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 +745,7 @@ public Optional getFetchDirective(final FetchDirectiv
* script-pre-request check
* @see
* script-post-request check
+ * @see #deliveredViaMeta()
*/
public boolean allowsExternalScript(
final Optional nonce,
@@ -728,7 +753,8 @@ public boolean allowsExternalScript(
final Optional extends URLWithScheme> scriptUrl,
final Optional parserInserted,
final Optional extends URLWithScheme> origin) {
- if (sandbox_ != null && !sandbox_.allowScripts()) {
+ final SandboxDirective sandbox = sandboxForQuery();
+ if (sandbox != null && !sandbox.allowScripts()) {
return false;
}
@@ -784,10 +810,12 @@ public boolean allowsExternalScript(
* @return {@code true} if this policy allows the inline script
* @see
* should block inline check
+ * @see #deliveredViaMeta()
*/
public boolean allowsInlineScript(final Optional nonce,
final Optional source, final Optional parserInserted) {
- if (sandbox_ != null && !sandbox_.allowScripts()) {
+ final SandboxDirective sandbox = sandboxForQuery();
+ if (sandbox != null && !sandbox.allowScripts()) {
return false;
}
return doesElementMatchSourceListForTypeAndSource(InlineType.Script, nonce, source, parserInserted);
@@ -802,9 +830,11 @@ public boolean allowsInlineScript(final Optional nonce,
* @return {@code true} if this policy allows the script attribute
* @see
* should block inline check (script-src-attr)
+ * @see #deliveredViaMeta()
*/
public boolean allowsScriptAsAttribute(final Optional source) {
- if (sandbox_ != null && !sandbox_.allowScripts()) {
+ final SandboxDirective sandbox = sandboxForQuery();
+ if (sandbox != null && !sandbox.allowScripts()) {
return false;
}
return doesElementMatchSourceListForTypeAndSource(
@@ -911,13 +941,15 @@ public boolean allowsNavigation(
* @return {@code true} if this policy allows the form action
* @see
* navigate-to pre-navigate check
+ * @see #deliveredViaMeta()
*/
public boolean allowsFormAction(
final Optional extends URLWithScheme> to,
final Optional redirected,
final Optional extends URLWithScheme> redirectedTo,
final Optional extends URLWithScheme> origin) {
- if (sandbox_ != null && !sandbox_.allowForms()) {
+ final SandboxDirective sandbox = sandboxForQuery();
+ if (sandbox != null && !sandbox.allowForms()) {
return false;
}
if (formAction_ != null) {
@@ -1043,7 +1075,9 @@ public boolean allowsFrame(final Optional extends URLWithScheme> source,
/**
* Determines whether this policy allows being framed by the given ancestor origin.
*
- * 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 +1085,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..ca43072 100644
--- a/src/main/java/org/htmlunit/csp/PolicyInOrigin.java
+++ b/src/main/java/org/htmlunit/csp/PolicyInOrigin.java
@@ -224,6 +224,17 @@ public boolean allowsUnsafeInlineStyle() {
return policy_.allowsInlineStyle(Optional.empty(), Optional.empty());
}
+ /**
+ * Determines whether the policy allows eval and similar string-to-code mechanisms.
+ *
+ * @return {@code true} if the policy allows eval
+ * @see Policy#allowsEval
+ * @since 5.4.0
+ */
+ public boolean allowsEval() {
+ return policy_.allowsEval();
+ }
+
/**
* Determines whether the policy allows a connection (e.g. {@code fetch()},
* {@code XMLHttpRequest}, {@code WebSocket}) to the given URL.
diff --git a/src/main/java/org/htmlunit/csp/PolicyList.java b/src/main/java/org/htmlunit/csp/PolicyList.java
index 2e441e5..28b6859 100644
--- a/src/main/java/org/htmlunit/csp/PolicyList.java
+++ b/src/main/java/org/htmlunit/csp/PolicyList.java
@@ -14,25 +14,38 @@
*/
package org.htmlunit.csp;
-import java.util.ArrayList;
import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+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.
*
*
* 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.
+ * An empty list (no policies) is treated as unrestricted: all {@code allows*}
+ * methods return {@code true}.
*
*
- * @see Policy#parseSerializedCSPList(String, Policy.PolicyListErrorConsumer)
+ * @author Ronald Brill
+ * @see Policy#parseSerializedCSPList(String, PolicyListErrorConsumer)
*/
public class PolicyList {
private final List policies_;
@@ -41,29 +54,56 @@ public class PolicyList {
* Ctor.
*
* @param policies the list of parsed {@link Policy} instances (empty policies excluded)
+ * @throws NullPointerException if {@code policies} is {@code null} or contains {@code null}
*/
public PolicyList(final List policies) {
- policies_ = policies;
+ policies_ = List.copyOf(policies);
}
/**
- * Returns a copy of the policies associated with this object.
+ * Parses and combines multiple serialized CSP strings into one policy 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 serializedPoliciesOrLists,
+ final PolicyListErrorConsumer policyListErrorConsumer) {
+ Objects.requireNonNull(serializedPoliciesOrLists, "serializedPoliciesOrLists");
+ Objects.requireNonNull(policyListErrorConsumer, "policyListErrorConsumer");
+ return Policy.parseSerializedCSPList(
+ String.join(",", serializedPoliciesOrLists), policyListErrorConsumer);
+ }
+
+ /**
+ * Returns the member policies (unmodifiable).
+ *
+ * @return an unmodifiable list of all policies
*/
public List getPolicies() {
- return new ArrayList(policies_);
+ return policies_;
}
/**
* Serializes this policy list back to its string representation.
*
* 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 +121,292 @@ public String toString() {
}
return out.toString();
}
+
+ // High-level querying: AND across all member policies
+
+ /**
+ * Whether every member policy allows the external script.
+ * @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 every member allows
+ * @see Policy#allowsExternalScript
+ * @since 5.4.0
+ */
+ public boolean allowsExternalScript(
+ final Optional nonce,
+ final Optional integrity,
+ final Optional extends URLWithScheme> scriptUrl,
+ final Optional parserInserted,
+ final Optional extends URLWithScheme> origin) {
+ return policies_.stream().allMatch(p ->
+ p.allowsExternalScript(nonce, integrity, scriptUrl, parserInserted, origin));
+ }
+
+ /**
+ * Whether every member policy allows the inline script.
+ * @param nonce script nonce, if any
+ * @param source inline script text, if known
+ * @param parserInserted whether the script is parser-inserted
+ * @return {@code true} if every member allows
+ * @see Policy#allowsInlineScript
+ * @since 5.4.0
+ */
+ public boolean allowsInlineScript(final Optional nonce,
+ final Optional source, final Optional parserInserted) {
+ return policies_.stream().allMatch(p -> p.allowsInlineScript(nonce, source, parserInserted));
+ }
+
+ /**
+ * Whether every member policy allows the script event-handler attribute.
+ * @param source attribute text, if known
+ * @return {@code true} if every member allows
+ * @see Policy#allowsScriptAsAttribute
+ * @since 5.4.0
+ */
+ public boolean allowsScriptAsAttribute(final Optional source) {
+ return policies_.stream().allMatch(p -> p.allowsScriptAsAttribute(source));
+ }
+
+ /**
+ * Whether every member policy allows eval.
+ * @return {@code true} if every member allows
+ * @see Policy#allowsEval
+ * @since 5.4.0
+ */
+ public boolean allowsEval() {
+ return policies_.stream().allMatch(Policy::allowsEval);
+ }
+
+ /**
+ * Whether every member policy allows the navigation.
+ * @param to navigation target URL, if known
+ * @param redirected whether the navigation is a redirect
+ * @param redirectedTo final URL after redirect, if known
+ * @param origin protected-resource origin, if known
+ * @return {@code true} if every member allows
+ * @see Policy#allowsNavigation
+ * @since 5.4.0
+ */
+ public boolean allowsNavigation(
+ final Optional extends URLWithScheme> to,
+ final Optional redirected,
+ final Optional extends URLWithScheme> redirectedTo,
+ final Optional extends URLWithScheme> origin) {
+ return policies_.stream().allMatch(p -> p.allowsNavigation(to, redirected, redirectedTo, origin));
+ }
+
+ /**
+ * Whether every member policy allows the form action.
+ * @param to form action URL, if known
+ * @param redirected whether the submission redirects
+ * @param redirectedTo final URL after redirect, if known
+ * @param origin protected-resource origin, if known
+ * @return {@code true} if every member allows
+ * @see Policy#allowsFormAction
+ * @since 5.4.0
+ */
+ public boolean allowsFormAction(
+ final Optional extends URLWithScheme> to,
+ final Optional redirected,
+ final Optional extends URLWithScheme> redirectedTo,
+ final Optional extends URLWithScheme> origin) {
+ return policies_.stream().allMatch(p -> p.allowsFormAction(to, redirected, redirectedTo, origin));
+ }
+
+ /**
+ * Whether every member policy allows the {@code javascript:} URL navigation.
+ * @param source JavaScript after the {@code javascript:} prefix, if known
+ * @param origin protected-resource origin, if known
+ * @return {@code true} if every member allows
+ * @see Policy#allowsJavascriptUrlNavigation
+ * @since 5.4.0
+ */
+ public boolean allowsJavascriptUrlNavigation(
+ final Optional source,
+ final Optional extends URLWithScheme> origin) {
+ return policies_.stream().allMatch(p -> p.allowsJavascriptUrlNavigation(source, origin));
+ }
+
+ /**
+ * Whether every member policy allows the external stylesheet.
+ * @param nonce link nonce, if any
+ * @param styleUrl stylesheet URL, if known
+ * @param origin protected-resource origin, if known
+ * @return {@code true} if every member allows
+ * @see Policy#allowsExternalStyle
+ * @since 5.4.0
+ */
+ public boolean allowsExternalStyle(
+ final Optional nonce,
+ final Optional extends URLWithScheme> styleUrl,
+ final Optional extends URLWithScheme> origin) {
+ return policies_.stream().allMatch(p -> p.allowsExternalStyle(nonce, styleUrl, origin));
+ }
+
+ /**
+ * Whether every member policy allows the inline style.
+ * @param nonce style nonce, if any
+ * @param source inline style text, if known
+ * @return {@code true} if every member allows
+ * @see Policy#allowsInlineStyle
+ * @since 5.4.0
+ */
+ public boolean allowsInlineStyle(final Optional nonce, final Optional source) {
+ return policies_.stream().allMatch(p -> p.allowsInlineStyle(nonce, source));
+ }
+
+ /**
+ * Whether every member policy allows the style attribute.
+ * @param source attribute text, if known
+ * @return {@code true} if every member allows
+ * @see Policy#allowsStyleAsAttribute
+ * @since 5.4.0
+ */
+ public boolean allowsStyleAsAttribute(final Optional source) {
+ return policies_.stream().allMatch(p -> p.allowsStyleAsAttribute(source));
+ }
+
+ /**
+ * Whether every member policy allows the frame.
+ * @param source framed resource URL, if known
+ * @param origin protected-resource origin, if known
+ * @return {@code true} if every member allows
+ * @see Policy#allowsFrame
+ * @since 5.4.0
+ */
+ public boolean allowsFrame(final Optional extends URLWithScheme> source,
+ final Optional extends URLWithScheme> origin) {
+ return policies_.stream().allMatch(p -> p.allowsFrame(source, origin));
+ }
+
+ /**
+ * Whether every member policy allows the frame ancestor.
+ * @param source ancestor frame URL, if known
+ * @param origin protected-resource origin, if known
+ * @return {@code true} if every member allows
+ * @see Policy#allowsFrameAncestor
+ * @since 5.4.0
+ */
+ public boolean allowsFrameAncestor(final Optional extends URLWithScheme> source,
+ final Optional extends URLWithScheme> origin) {
+ return policies_.stream().allMatch(p -> p.allowsFrameAncestor(source, origin));
+ }
+
+ /**
+ * Whether every member policy allows the connection.
+ * @param source connection URL, if known
+ * @param origin protected-resource origin, if known
+ * @return {@code true} if every member allows
+ * @see Policy#allowsConnection
+ * @since 5.4.0
+ */
+ public boolean allowsConnection(final Optional extends URLWithScheme> source,
+ final Optional extends URLWithScheme> origin) {
+ return policies_.stream().allMatch(p -> p.allowsConnection(source, origin));
+ }
+
+ /**
+ * Whether every member policy allows the font.
+ * @param source font URL, if known
+ * @param origin protected-resource origin, if known
+ * @return {@code true} if every member allows
+ * @see Policy#allowsFont
+ * @since 5.4.0
+ */
+ public boolean allowsFont(final Optional extends URLWithScheme> source,
+ final Optional extends URLWithScheme> origin) {
+ return policies_.stream().allMatch(p -> p.allowsFont(source, origin));
+ }
+
+ /**
+ * Whether every member policy allows the image.
+ * @param source image URL, if known
+ * @param origin protected-resource origin, if known
+ * @return {@code true} if every member allows
+ * @see Policy#allowsImage
+ * @since 5.4.0
+ */
+ public boolean allowsImage(final Optional extends URLWithScheme> source,
+ final Optional extends URLWithScheme> origin) {
+ return policies_.stream().allMatch(p -> p.allowsImage(source, origin));
+ }
+
+ /**
+ * Whether every member policy allows the application manifest.
+ * @param source manifest URL, if known
+ * @param origin protected-resource origin, if known
+ * @return {@code true} if every member allows
+ * @see Policy#allowsApplicationManifest
+ * @since 5.4.0
+ */
+ public boolean allowsApplicationManifest(final Optional extends URLWithScheme> source,
+ final Optional extends URLWithScheme> origin) {
+ return policies_.stream().allMatch(p -> p.allowsApplicationManifest(source, origin));
+ }
+
+ /**
+ * Whether every member policy allows the media.
+ * @param source media URL, if known
+ * @param origin protected-resource origin, if known
+ * @return {@code true} if every member allows
+ * @see Policy#allowsMedia
+ * @since 5.4.0
+ */
+ public boolean allowsMedia(final Optional extends URLWithScheme> source,
+ final Optional extends URLWithScheme> origin) {
+ return policies_.stream().allMatch(p -> p.allowsMedia(source, origin));
+ }
+
+ /**
+ * Whether every member policy allows the object.
+ * @param source object URL, if known
+ * @param origin protected-resource origin, if known
+ * @return {@code true} if every member allows
+ * @see Policy#allowsObject
+ * @since 5.4.0
+ */
+ public boolean allowsObject(final Optional extends URLWithScheme> source,
+ final Optional extends URLWithScheme> origin) {
+ return policies_.stream().allMatch(p -> p.allowsObject(source, origin));
+ }
+
+ /**
+ * Whether every member policy allows the prefetch.
+ * @param source prefetch URL, if known
+ * @param origin protected-resource origin, if known
+ * @return {@code true} if every member allows
+ * @see Policy#allowsPrefetch
+ * @since 5.4.0
+ */
+ public boolean allowsPrefetch(final Optional extends URLWithScheme> source,
+ final Optional extends URLWithScheme> origin) {
+ return policies_.stream().allMatch(p -> p.allowsPrefetch(source, origin));
+ }
+
+ /**
+ * Whether every member policy allows the worker.
+ * @param source worker URL, if known
+ * @param origin protected-resource origin, if known
+ * @return {@code true} if every member allows
+ * @see Policy#allowsWorker
+ * @since 5.4.0
+ */
+ public boolean allowsWorker(final Optional extends URLWithScheme> source,
+ final Optional extends URLWithScheme> origin) {
+ return policies_.stream().allMatch(p -> p.allowsWorker(source, origin));
+ }
+
+ /**
+ * Whether every member policy allows the plugin type.
+ * @param mediaType plugin media type, if known
+ * @return {@code true} if every member allows
+ * @see Policy#allowsPlugin
+ * @since 5.4.0
+ */
+ public boolean allowsPlugin(final Optional extends MediaType> mediaType) {
+ return policies_.stream().allMatch(p -> p.allowsPlugin(mediaType));
+ }
}
diff --git a/src/main/java/org/htmlunit/csp/PolicyListInOrigin.java b/src/main/java/org/htmlunit/csp/PolicyListInOrigin.java
new file mode 100644
index 0000000..220afba
--- /dev/null
+++ b/src/main/java/org/htmlunit/csp/PolicyListInOrigin.java
@@ -0,0 +1,238 @@
+/*
+ * 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;
+
+/**
+ * A convenience wrapper that pairs a {@link PolicyList} with its
+ * {@link URLWithScheme origin}, providing simplified query methods that
+ * automatically supply the origin to the underlying list checks.
+ *
+ * Each {@code allows*} method delegates to the corresponding method on
+ * {@link PolicyList}, which returns {@code true} only when every member
+ * {@link Policy} allows the resource.
+ *
+ *
+ * @author Ronald Brill
+ * @see PolicyInOrigin
+ * @see PolicyList
+ * @since 5.4.0
+ */
+public class PolicyListInOrigin {
+ private final PolicyList policyList_;
+ private final URLWithScheme origin_;
+
+ /**
+ * Ctor.
+ *
+ * @param policyList the Content Security Policy list to query against
+ * @param origin the origin of the protected resource
+ */
+ public PolicyListInOrigin(final PolicyList policyList, final URLWithScheme origin) {
+ policyList_ = policyList;
+ origin_ = origin;
+ }
+
+ /**
+ * Returns the underlying {@link PolicyList}.
+ *
+ * @return the policy list associated with this origin-bound wrapper
+ */
+ public PolicyList getPolicyList() {
+ return policyList_;
+ }
+
+ /**
+ * Returns the underlying origin.
+ *
+ * @return the origin of the protected resource
+ */
+ public URLWithScheme getOrigin() {
+ return origin_;
+ }
+
+ /**
+ * Determines whether every policy allows loading an external script from the given URL.
+ *
+ * @param url the URL of the external script
+ * @return {@code true} if every member policy allows the script from the given source
+ */
+ public boolean allowsScriptFromSource(final URLWithScheme url) {
+ return policyList_.allowsExternalScript(Optional.empty(),
+ Optional.empty(), Optional.of(url), Optional.empty(), Optional.of(origin_));
+ }
+
+ /**
+ * Determines whether every policy allows loading an external stylesheet from the given URL.
+ *
+ * @param url the URL of the external stylesheet
+ * @return {@code true} if every member policy allows the style from the given source
+ */
+ public boolean allowsStyleFromSource(final URLWithScheme url) {
+ return policyList_.allowsExternalStyle(Optional.empty(), Optional.of(url), Optional.of(origin_));
+ }
+
+ /**
+ * Determines whether every policy allows loading an image from the given URL.
+ *
+ * @param url the URL of the image resource
+ * @return {@code true} if every member policy allows the image from the given source
+ */
+ public boolean allowsImageFromSource(final URLWithScheme url) {
+ return policyList_.allowsImage(Optional.of(url), Optional.of(origin_));
+ }
+
+ /**
+ * Determines whether every policy allows loading a frame from the given URL.
+ *
+ * @param url the URL of the framed resource
+ * @return {@code true} if every member policy allows the frame from the given source
+ */
+ public boolean allowsFrameFromSource(final URLWithScheme url) {
+ return policyList_.allowsFrame(Optional.of(url), Optional.of(origin_));
+ }
+
+ /**
+ * Determines whether every policy allows loading a worker from the given URL.
+ *
+ * @param url the URL of the worker script
+ * @return {@code true} if every member policy allows the worker from the given source
+ */
+ public boolean allowsWorkerFromSource(final URLWithScheme url) {
+ return policyList_.allowsWorker(Optional.of(url), Optional.of(origin_));
+ }
+
+ /**
+ * Determines whether every policy allows loading a font from the given URL.
+ *
+ * @param url the URL of the font resource
+ * @return {@code true} if every member policy allows the font from the given source
+ */
+ public boolean allowsFontFromSource(final URLWithScheme url) {
+ return policyList_.allowsFont(Optional.of(url), Optional.of(origin_));
+ }
+
+ /**
+ * Determines whether every policy allows loading an object/embed/applet from the given URL.
+ *
+ * @param url the URL of the object resource
+ * @return {@code true} if every member policy allows the object from the given source
+ */
+ public boolean allowsObjectFromSource(final URLWithScheme url) {
+ return policyList_.allowsObject(Optional.of(url), Optional.of(origin_));
+ }
+
+ /**
+ * Determines whether every policy allows loading media (audio/video) from the given URL.
+ *
+ * @param url the URL of the media resource
+ * @return {@code true} if every member policy allows the media from the given source
+ */
+ public boolean allowsMediaFromSource(final URLWithScheme url) {
+ return policyList_.allowsMedia(Optional.of(url), Optional.of(origin_));
+ }
+
+ /**
+ * Determines whether every policy allows loading an application manifest from the given URL.
+ *
+ * @param url the URL of the manifest resource
+ * @return {@code true} if every member policy allows the manifest from the given source
+ */
+ public boolean allowsManifestFromSource(final URLWithScheme url) {
+ return policyList_.allowsApplicationManifest(Optional.of(url), Optional.of(origin_));
+ }
+
+ /**
+ * Determines whether every policy allows a prefetch from the given URL.
+ *
+ * @param url the URL to prefetch
+ * @return {@code true} if every member policy allows the prefetch from the given source
+ */
+ public boolean allowsPrefetchFromSource(final URLWithScheme url) {
+ return policyList_.allowsPrefetch(Optional.of(url), Optional.of(origin_));
+ }
+
+ /**
+ * Determines whether every policy allows any inline script without a nonce or hash.
+ *
+ * @return {@code true} if every member policy allows inline scripts without specific credentials
+ */
+ public boolean allowsUnsafeInlineScript() {
+ return policyList_.allowsInlineScript(Optional.empty(), Optional.empty(), Optional.empty());
+ }
+
+ /**
+ * Determines whether every policy allows any inline style without a nonce or hash.
+ *
+ * @return {@code true} if every member policy allows inline styles without specific credentials
+ */
+ public boolean allowsUnsafeInlineStyle() {
+ return policyList_.allowsInlineStyle(Optional.empty(), Optional.empty());
+ }
+
+ /**
+ * Determines whether every policy allows a connection to the given URL.
+ *
+ * @param url the URL to connect to
+ * @return {@code true} if every member policy allows the connection to the given source
+ */
+ public boolean allowsConnection(final URLWithScheme url) {
+ return policyList_.allowsConnection(Optional.of(url), Optional.of(origin_));
+ }
+
+ /**
+ * Determines whether every policy allows navigation to the given URL.
+ *
+ * @param url the navigation target URL
+ * @return {@code true} if every member policy allows navigation to the given URL
+ */
+ public boolean allowsNavigation(final URLWithScheme url) {
+ return policyList_.allowsNavigation(Optional.of(url),
+ Optional.empty(), Optional.empty(), Optional.of(origin_));
+ }
+
+ /**
+ * Determines whether every policy allows being framed by the given ancestor URL.
+ *
+ * @param url the URL of the ancestor frame
+ * @return {@code true} if every member policy allows the frame ancestor
+ */
+ public boolean allowsFrameAncestor(final URLWithScheme url) {
+ return policyList_.allowsFrameAncestor(Optional.of(url), Optional.of(origin_));
+ }
+
+ /**
+ * Determines whether every policy allows a form submission to the given URL.
+ *
+ * @param url the form action target URL
+ * @return {@code true} if every member policy allows the form action to the given URL
+ */
+ public boolean allowsFormAction(final URLWithScheme url) {
+ return policyList_.allowsFormAction(Optional.of(url),
+ Optional.empty(), Optional.empty(), Optional.of(origin_));
+ }
+
+ /**
+ * Determines whether every policy allows eval.
+ *
+ * @return {@code true} if every member policy allows eval
+ */
+ public boolean allowsEval() {
+ return policyList_.allowsEval();
+ }
+}
diff --git a/src/test/java/org/htmlunit/csp/PolicyListQueryingTest.java b/src/test/java/org/htmlunit/csp/PolicyListQueryingTest.java
new file mode 100644
index 0000000..e3811bc
--- /dev/null
+++ b/src/test/java/org/htmlunit/csp/PolicyListQueryingTest.java
@@ -0,0 +1,619 @@
+/*
+ * 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 static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+import org.htmlunit.csp.Policy.PolicyListErrorConsumer;
+import org.htmlunit.csp.url.URI;
+import org.htmlunit.csp.url.URLWithScheme;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Multi-policy query-time AND behavior on {@link PolicyList} /
+ * {@link PolicyListInOrigin}.
+ *
+ * @see CSP3 multiple policies
+ * @see Foundeo examples
+ */
+public class PolicyListQueryingTest extends TestBase {
+
+ private static final URLWithScheme ORIGIN = URI.parseURI("https://example.com").orElseThrow();
+
+ @Test
+ public void ambiguousImgSrcHostsEffectivelyNone() {
+ // Content-Security-Policy: img-src img.example.com;
+ // Content-Security-Policy: img-src more-images.example.com;
+ final PolicyListInOrigin list = ofSerialized(
+ List.of("img-src img.example.com", "img-src more-images.example.com"));
+
+ assertFalse(list.allowsImageFromSource(uri("https://img.example.com/a.png")));
+ assertFalse(list.allowsImageFromSource(uri("https://more-images.example.com/b.png")));
+ assertFalse(list.allowsImageFromSource(uri("https://other.example.com/c.png")));
+ assertEquals(2, list.getPolicyList().getPolicies().size());
+ }
+
+ @Test
+ public void nestedRestrictivenessBlocksExtraHost() {
+ // img-src 'self' AND img-src 'self' img.example.com → only 'self' survives
+ final PolicyListInOrigin list = ofSerialized(
+ List.of("img-src 'self'", "img-src 'self' img.example.com"));
+
+ assertTrue(list.allowsImageFromSource(uri("https://example.com/logo.png")));
+ assertFalse(list.allowsImageFromSource(uri("https://img.example.com/a.png")));
+ }
+
+ @Test
+ public void defaultSrcFallbackAcrossPolicies() {
+ // default-src 'self' AND img-src 'self' img.example.com
+ final PolicyListInOrigin list = ofSerialized(
+ List.of("default-src 'self'", "img-src 'self' img.example.com"));
+
+ assertTrue(list.allowsImageFromSource(uri("https://example.com/logo.png")));
+ assertFalse(list.allowsImageFromSource(uri("https://img.example.com/a.png")));
+ }
+
+ /**
+ * Both policies omit the fetch directive, so each falls back to its own
+ * {@code default-src}. AND is the overlap of those default-src lists.
+ */
+ @Test
+ public void defaultSrcOverlapWhenBothFallBack() {
+ // default-src a.com ∩ default-src a.com b.com → only a.com for images
+ final PolicyListInOrigin hostOverlap = ofSerialized(List.of(
+ "default-src https://a.example.com",
+ "default-src https://a.example.com https://b.example.com"));
+
+ assertTrue(hostOverlap.allowsImageFromSource(uri("https://a.example.com/i.png")));
+ assertFalse(hostOverlap.allowsImageFromSource(uri("https://b.example.com/i.png")));
+ assertTrue(hostOverlap.allowsScriptFromSource(uri("https://a.example.com/app.js")));
+ assertFalse(hostOverlap.allowsScriptFromSource(uri("https://b.example.com/app.js")));
+
+ // Asymmetric: one side only 'self', other allows a third-party via default-src
+ final PolicyListInOrigin asymmetric = ofSerialized(List.of(
+ "default-src 'self'",
+ "default-src 'self' https://cdn.example.com"));
+
+ assertTrue(asymmetric.allowsFontFromSource(uri("https://example.com/f.woff2")));
+ assertFalse(asymmetric.allowsFontFromSource(uri("https://cdn.example.com/f.woff2")));
+
+ // Both default-src 'none' → nothing allowed for fetch types that fall back
+ final PolicyListInOrigin bothNone = ofSerialized(List.of(
+ "default-src 'none'",
+ "default-src 'none'"));
+
+ assertFalse(bothNone.allowsImageFromSource(uri("https://example.com/i.png")));
+ assertFalse(bothNone.allowsScriptFromSource(uri("https://example.com/app.js")));
+ assertFalse(bothNone.allowsConnection(uri("https://example.com/api")));
+ }
+
+ @Test
+ public void orderDoesNotMatter() {
+ final PolicyListInOrigin aThenB = ofSerialized(
+ List.of("img-src img.example.com", "img-src more-images.example.com"));
+ final PolicyListInOrigin bThenA = ofSerialized(
+ List.of("img-src more-images.example.com", "img-src img.example.com"));
+
+ final URLWithScheme img = uri("https://img.example.com/a.png");
+ final URLWithScheme more = uri("https://more-images.example.com/b.png");
+ assertEquals(aThenB.allowsImageFromSource(img), bThenA.allowsImageFromSource(img));
+ assertEquals(aThenB.allowsImageFromSource(more), bThenA.allowsImageFromSource(more));
+ }
+
+ @Test
+ public void overlappingHostsAllowed() {
+ final PolicyListInOrigin list = ofSerialized(
+ List.of("img-src https://cdn.example.com", "img-src https://cdn.example.com https://other.example.com"));
+
+ assertTrue(list.allowsImageFromSource(uri("https://cdn.example.com/a.png")));
+ assertFalse(list.allowsImageFromSource(uri("https://other.example.com/a.png")));
+ }
+
+ @Test
+ public void emptyListAllowsEverything() {
+ final PolicyList empty = new PolicyList(Collections.emptyList());
+ assertTrue(empty.allowsImage(Optional.of(uri("https://anywhere.example/x")), Optional.of(ORIGIN)));
+ assertTrue(empty.allowsEval());
+ assertTrue(new PolicyListInOrigin(empty, ORIGIN).allowsUnsafeInlineScript());
+ assertThrows(UnsupportedOperationException.class,
+ () -> empty.getPolicies().add(Policy.parseSerializedCSP("default-src 'none'",
+ Policy.PolicyErrorConsumer.ignored)));
+ }
+
+ @Test
+ public void ofSerializedParsesCommaSeparatedHeaderValues() {
+ // One header value that is itself a CSP list, plus another header
+ final PolicyList list = PolicyList.ofSerialized(
+ List.of("img-src a.example, script-src 'none'", "connect-src 'self'"),
+ PolicyListErrorConsumer.ignored);
+
+ assertEquals(3, list.getPolicies().size());
+ }
+
+ @Test
+ public void unsafeInlineRequiresAllPolicies() {
+ final PolicyListInOrigin bothAllow = ofSerialized(
+ List.of("script-src 'unsafe-inline'", "default-src 'unsafe-inline'"));
+ assertTrue(bothAllow.allowsUnsafeInlineScript());
+
+ final PolicyListInOrigin oneBlocks = ofSerialized(
+ List.of("script-src 'unsafe-inline'", "script-src 'none'"));
+ assertFalse(oneBlocks.allowsUnsafeInlineScript());
+ }
+
+ @Test
+ public void allowsEvalRequiresAllPolicies() {
+ final PolicyList bothAllow = PolicyList.ofSerialized(
+ List.of("script-src 'unsafe-eval'", "default-src 'unsafe-eval'"),
+ PolicyListErrorConsumer.ignored);
+ assertTrue(bothAllow.allowsEval());
+
+ final PolicyList oneBlocks = PolicyList.ofSerialized(
+ List.of("script-src 'unsafe-eval'", "script-src 'none'"),
+ PolicyListErrorConsumer.ignored);
+ assertFalse(oneBlocks.allowsEval());
+ }
+
+ @Test
+ public void parseSerializedCSPListAlsoAnds() {
+ final PolicyList list = Policy.parseSerializedCSPList(
+ "img-src img.example.com, img-src more-images.example.com",
+ PolicyListErrorConsumer.ignored);
+ final PolicyListInOrigin bound = new PolicyListInOrigin(list, ORIGIN);
+
+ assertFalse(bound.allowsImageFromSource(uri("https://img.example.com/a.png")));
+ assertFalse(bound.allowsImageFromSource(uri("https://more-images.example.com/b.png")));
+ }
+
+ /**
+ * Typical SaaS baseline (self + CDNs + analytics) plus a second edge/WAF header that
+ * drops third-party scripts and connections.
+ */
+ @Test
+ public void saasBaselineTightenedByEdgeHeader() {
+ final String appPolicy = String.join(" ",
+ "default-src 'self';",
+ "script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://www.googletagmanager.com;",
+ "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;",
+ "font-src 'self' https://fonts.gstatic.com data:;",
+ "img-src 'self' data: https: blob:;",
+ "connect-src 'self' https://api.example.com https://www.google-analytics.com;",
+ "frame-src 'self' https://js.stripe.com;",
+ "object-src 'none';",
+ "base-uri 'self';",
+ "form-action 'self';",
+ "frame-ancestors 'self';",
+ "upgrade-insecure-requests");
+
+ final String edgeTighten = String.join(" ",
+ "default-src 'self';",
+ "script-src 'self';",
+ "style-src 'self' 'unsafe-inline';",
+ "font-src 'self' data:;",
+ "img-src 'self' data: https:;",
+ "connect-src 'self' https://api.example.com;",
+ "frame-src 'self';",
+ "object-src 'none';",
+ "base-uri 'self';",
+ "form-action 'self';",
+ "frame-ancestors 'self'");
+
+ final PolicyListInOrigin list = ofSerializedIgnored(List.of(appPolicy, edgeTighten));
+
+ assertTrue(list.allowsScriptFromSource(uri("https://example.com/app.js")));
+ assertFalse(list.allowsScriptFromSource(uri("https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js")));
+ assertFalse(list.allowsScriptFromSource(uri("https://www.googletagmanager.com/gtm.js")));
+ assertTrue(list.allowsConnection(uri("https://api.example.com/v1/users")));
+ assertFalse(list.allowsConnection(uri("https://www.google-analytics.com/g/collect")));
+ assertFalse(list.allowsFrameFromSource(uri("https://js.stripe.com/v3/")));
+ assertTrue(list.allowsImageFromSource(uri("https://cdn.example.net/hero.webp")));
+ assertFalse(list.allowsUnsafeInlineScript());
+ assertTrue(list.allowsUnsafeInlineStyle());
+ assertFalse(list.allowsEval());
+ }
+
+ /**
+ * Marketing stack (GTM / GA / fonts) intersected with a stricter product policy that
+ * only permits first-party scripts — common when product and marketing CSPs both ship.
+ */
+ @Test
+ public void marketingStackIntersectedWithProductLockdown() {
+ final String marketing = String.join(" ",
+ "default-src 'none';",
+ "script-src 'self' https://www.googletagmanager.com https://www.google-analytics.com"
+ + " https://tagmanager.google.com 'unsafe-inline';",
+ "style-src 'self' https://fonts.googleapis.com https://tagmanager.google.com 'unsafe-inline';",
+ "font-src 'self' https://fonts.gstatic.com data:;",
+ "img-src 'self' data: https://www.google-analytics.com https://www.googletagmanager.com"
+ + " https://*.googleusercontent.com;",
+ "connect-src 'self' https://www.google-analytics.com https://analytics.google.com"
+ + " https://stats.g.doubleclick.net;",
+ "frame-src https://www.googletagmanager.com;");
+
+ final String product = String.join(" ",
+ "default-src 'self';",
+ "script-src 'self';",
+ "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;",
+ "font-src 'self' https://fonts.gstatic.com;",
+ "img-src 'self' data: https:;",
+ "connect-src 'self';",
+ "frame-src 'none';",
+ "object-src 'none'");
+
+ final PolicyListInOrigin list = ofSerializedIgnored(List.of(marketing, product));
+
+ assertTrue(list.allowsScriptFromSource(uri("https://example.com/bundle.js")));
+ assertFalse(list.allowsScriptFromSource(uri("https://www.googletagmanager.com/gtm.js")));
+ assertTrue(list.allowsStyleFromSource(uri("https://fonts.googleapis.com/css2?family=Inter")));
+ assertTrue(list.allowsFontFromSource(uri("https://fonts.gstatic.com/s/inter/v12/font.woff2")));
+ assertFalse(list.allowsConnection(uri("https://www.google-analytics.com/g/collect")));
+ assertFalse(list.allowsFrameFromSource(uri("https://www.googletagmanager.com/ns.html")));
+ assertFalse(list.allowsUnsafeInlineScript());
+ assertTrue(list.allowsUnsafeInlineStyle());
+ }
+
+ /**
+ * Enforce header + page meta policy (meta often used for app-generated directives).
+ * Intersection still applies; {@code report-uri} / {@code frame-ancestors} only on
+ * the header policy (and meta is parsed with {@code deliveredViaMeta = true}).
+ */
+ @Test
+ public void headerPlusMetaDualDelivery() {
+ final String headerText = String.join(" ",
+ "default-src 'self';",
+ "script-src 'self' https://cdn.example.com;",
+ "img-src 'self' https://images.example.com;",
+ "connect-src 'self';",
+ "frame-ancestors 'none';",
+ "report-uri /csp-report");
+
+ final String metaText = String.join(" ",
+ "default-src 'self';",
+ "script-src 'self';",
+ "img-src 'self' https://images.example.com https://cdn.example.com;",
+ "connect-src 'self' https://api.example.com");
+
+ final Policy header = Policy.parseSerializedCSP(
+ headerText, Policy.PolicyErrorConsumer.ignored, false);
+ final Policy meta = Policy.parseSerializedCSP(
+ metaText, Policy.PolicyErrorConsumer.ignored, true);
+ assertTrue(meta.deliveredViaMeta());
+ assertFalse(header.deliveredViaMeta());
+
+ final PolicyListInOrigin list = new PolicyListInOrigin(
+ new PolicyList(List.of(header, meta)), ORIGIN);
+
+ assertTrue(list.allowsScriptFromSource(uri("https://example.com/main.js")));
+ assertFalse(list.allowsScriptFromSource(uri("https://cdn.example.com/vendor.js")));
+ assertTrue(list.allowsImageFromSource(uri("https://images.example.com/a.png")));
+ assertFalse(list.allowsImageFromSource(uri("https://cdn.example.com/b.png")));
+ assertFalse(list.allowsConnection(uri("https://api.example.com/x")));
+ assertFalse(list.allowsFrameAncestor(uri("https://evil.example/")));
+ assertFalse(list.allowsFrameAncestor(uri("https://example.com/")));
+ }
+
+ /**
+ * {@code navigate-to} / {@code javascript:} URL navigation must pass every policy.
+ */
+ @Test
+ public void navigationAndJavascriptUrlRequireAllPolicies() {
+ final PolicyListInOrigin nav = ofSerialized(List.of(
+ "navigate-to https://a.example.com",
+ "navigate-to https://a.example.com https://b.example.com"));
+
+ assertTrue(nav.allowsNavigation(uri("https://a.example.com/page")));
+ assertFalse(nav.allowsNavigation(uri("https://b.example.com/page")));
+ assertFalse(nav.allowsNavigation(uri("https://c.example.com/page")));
+
+ final PolicyList bothAllowJs = PolicyList.ofSerialized(
+ List.of(
+ "script-src 'unsafe-inline'; navigate-to javascript:",
+ "default-src 'unsafe-inline'; navigate-to javascript:"),
+ PolicyListErrorConsumer.ignored);
+ assertTrue(bothAllowJs.allowsJavascriptUrlNavigation(
+ Optional.of("alert(1)"), Optional.of(ORIGIN)));
+
+ final PolicyList oneBlocksJs = PolicyList.ofSerialized(
+ List.of(
+ "script-src 'unsafe-inline'; navigate-to javascript:",
+ "navigate-to 'none'"),
+ PolicyListErrorConsumer.ignored);
+ assertFalse(oneBlocksJs.allowsJavascriptUrlNavigation(
+ Optional.of("alert(1)"), Optional.of(ORIGIN)));
+ }
+
+ /**
+ * Meta {@code frame-ancestors} / {@code sandbox} are ignored for query; header
+ * copies still AND. Fetch directives on the meta policy still participate.
+ */
+ @Test
+ public void headerPlusMetaIgnoresMetaInvalidDirectivesForQuery() {
+ final Policy header = Policy.parseSerializedCSP(
+ "default-src 'self'; frame-ancestors 'none'", Policy.PolicyErrorConsumer.ignored, false);
+ final Policy meta = Policy.parseSerializedCSP(
+ "script-src 'self'; frame-ancestors 'none'; sandbox",
+ Policy.PolicyErrorConsumer.ignored, true);
+
+ final PolicyListInOrigin list = new PolicyListInOrigin(
+ new PolicyList(List.of(header, meta)), ORIGIN);
+
+ // Meta sandbox does not block scripts; both policies allow 'self' scripts
+ assertTrue(list.allowsScriptFromSource(uri("https://example.com/app.js")));
+ assertFalse(list.allowsScriptFromSource(uri("https://cdn.example.com/x.js")));
+
+ // Header frame-ancestors 'none' still blocks; meta's copy is ignored for query
+ assertFalse(list.allowsFrameAncestor(uri("https://evil.example/")));
+ assertFalse(list.allowsFrameAncestor(uri("https://example.com/")));
+
+ // Meta-only invalid directives: no header frame-ancestors → allow ancestors;
+ // meta sandbox does not block forms/scripts by itself
+ final Policy headerFetchOnly = Policy.parseSerializedCSP(
+ "script-src 'self'", Policy.PolicyErrorConsumer.ignored, false);
+ final PolicyListInOrigin metaFaOnly = new PolicyListInOrigin(
+ new PolicyList(List.of(headerFetchOnly, meta)), ORIGIN);
+ assertTrue(metaFaOnly.allowsFrameAncestor(uri("https://evil.example/")));
+ assertTrue(metaFaOnly.allowsScriptFromSource(uri("https://example.com/app.js")));
+ assertTrue(metaFaOnly.allowsFormAction(uri("https://example.com/submit")));
+ }
+
+ /**
+ * Busy multi-CDN allowlists with only partial host overlap across two headers.
+ */
+ @Test
+ public void multiCdnAllowlistsPartialOverlap() {
+ final String policyA = String.join(" ",
+ "default-src 'none';",
+ "script-src 'self' https://cdn.jsdelivr.net https://unpkg.com https://cdnjs.cloudflare.com;",
+ "style-src 'self' https://cdn.jsdelivr.net https://fonts.googleapis.com;",
+ "font-src https://fonts.gstatic.com https://cdn.jsdelivr.net;",
+ "img-src 'self' https://cdn.jsdelivr.net data: blob:;",
+ "worker-src 'self' blob:;");
+
+ final String policyB = String.join(" ",
+ "default-src 'none';",
+ "script-src 'self' https://cdn.jsdelivr.net https://static.cloudflareinsights.com;",
+ "style-src 'self' https://fonts.googleapis.com;",
+ "font-src https://fonts.gstatic.com;",
+ "img-src 'self' data:;",
+ "worker-src 'self';");
+
+ final PolicyListInOrigin list = ofSerializedIgnored(List.of(policyA, policyB));
+
+ assertTrue(list.allowsScriptFromSource(uri("https://cdn.jsdelivr.net/npm/lodash@4/lodash.min.js")));
+ assertFalse(list.allowsScriptFromSource(uri("https://unpkg.com/react@18/umd/react.production.min.js")));
+ assertFalse(list.allowsScriptFromSource(uri("https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js")));
+ assertFalse(list.allowsScriptFromSource(uri("https://static.cloudflareinsights.com/beacon.min.js")));
+ assertTrue(list.allowsStyleFromSource(uri("https://fonts.googleapis.com/css?family=Roboto")));
+ assertFalse(list.allowsStyleFromSource(uri("https://cdn.jsdelivr.net/npm/bootstrap@5/dist/css/bootstrap.min.css")));
+ assertTrue(list.allowsFontFromSource(uri("https://fonts.gstatic.com/s/roboto/v30/font.woff2")));
+ assertFalse(list.allowsImageFromSource(uri("https://cdn.jsdelivr.net/gh/foo/bar/logo.png")));
+ assertTrue(list.allowsWorkerFromSource(uri("https://example.com/sw.js")));
+ assertFalse(list.allowsWorkerFromSource(uri("https://cdn.jsdelivr.net/worker.js")));
+ }
+
+ /**
+ * Wildcard host in one policy, concrete host in the other — intersection is hosts that
+ * match the wildcard and appear (or are covered) in the second policy.
+ */
+ @Test
+ public void wildcardHostIntersectConcreteHosts() {
+ final PolicyListInOrigin list = ofSerializedIgnored(List.of(
+ "script-src https://*.example.net https://cdn.partner.com",
+ "script-src https://assets.example.net https://cdn.partner.com https://extra.example.net"));
+
+ assertTrue(list.allowsScriptFromSource(uri("https://assets.example.net/app.js")));
+ assertTrue(list.allowsScriptFromSource(uri("https://cdn.partner.com/lib.js")));
+ assertTrue(list.allowsScriptFromSource(uri("https://extra.example.net/x.js")));
+ assertFalse(list.allowsScriptFromSource(uri("https://other.example.net/x.js")));
+ assertFalse(list.allowsScriptFromSource(uri("https://cdn.other.com/x.js")));
+ }
+
+ /**
+ * Two host-source wildcards AND'd: a URL must match both patterns.
+ * Nested wildcards share hosts in the overlap; disjoint wildcards allow nothing.
+ */
+ @Test
+ public void wildcardHostIntersectWildcardHost() {
+ // Nested: *.cdn.example.com ⊂ *.example.com (for matching purposes)
+ final PolicyListInOrigin nested = ofSerializedIgnored(List.of(
+ "script-src https://*.example.com",
+ "script-src https://*.cdn.example.com"));
+
+ assertTrue(nested.allowsScriptFromSource(uri("https://assets.cdn.example.com/app.js")));
+ assertTrue(nested.allowsScriptFromSource(uri("https://a.b.cdn.example.com/x.js")));
+ assertFalse(nested.allowsScriptFromSource(uri("https://www.example.com/app.js")));
+ assertFalse(nested.allowsScriptFromSource(uri("https://cdn.example.com/app.js")));
+ assertFalse(nested.allowsScriptFromSource(uri("https://other.example.net/app.js")));
+
+ // Disjoint registrable domains → no host satisfies both
+ final PolicyListInOrigin disjoint = ofSerializedIgnored(List.of(
+ "img-src https://*.example.com",
+ "img-src https://*.example.net"));
+
+ assertFalse(disjoint.allowsImageFromSource(uri("https://a.example.com/i.png")));
+ assertFalse(disjoint.allowsImageFromSource(uri("https://a.example.net/i.png")));
+ assertFalse(disjoint.allowsImageFromSource(uri("https://a.example.org/i.png")));
+
+ // Scheme-part-match: http source allows https URL; https source does not allow http URL
+ final PolicyListInOrigin schemeAndHost = ofSerializedIgnored(List.of(
+ "script-src https://*.assets.example.com",
+ "script-src http://*.assets.example.com"));
+
+ assertTrue(schemeAndHost.allowsScriptFromSource(uri("https://cdn.assets.example.com/lib.js")));
+ assertFalse(schemeAndHost.allowsScriptFromSource(uri("http://cdn.assets.example.com/lib.js")));
+
+ // https://* ∩ https://*.partner.com
+ final PolicyListInOrigin starScheme = ofSerializedIgnored(List.of(
+ "script-src https://*",
+ "script-src https://*.partner.com"));
+
+ assertTrue(starScheme.allowsScriptFromSource(uri("https://cdn.partner.com/a.js")));
+ assertFalse(starScheme.allowsScriptFromSource(uri("https://cdn.other.com/a.js")));
+ assertFalse(starScheme.allowsScriptFromSource(uri("http://cdn.partner.com/a.js")));
+ }
+
+ /**
+ * Three policies (primary app, payment iframe vendor allowlist, reporting/monitoring
+ * lockdown) — resource must pass all three.
+ */
+ @Test
+ public void threeLayerCorporatePolicies() {
+ final String app = String.join(" ",
+ "default-src 'self';",
+ "script-src 'self' https://js.stripe.com https://checkout.paypal.com;",
+ "frame-src 'self' https://js.stripe.com https://hooks.stripe.com https://www.paypal.com;",
+ "connect-src 'self' https://api.stripe.com https://api.paypal.com;",
+ "img-src 'self' data: https:;",
+ "style-src 'self' 'unsafe-inline';",
+ "object-src 'none'");
+
+ final String paymentsOnly = String.join(" ",
+ "default-src 'none';",
+ "script-src 'self' https://js.stripe.com;",
+ "frame-src https://js.stripe.com https://hooks.stripe.com;",
+ "connect-src 'self' https://api.stripe.com;",
+ "img-src 'self' data:;",
+ "style-src 'self' 'unsafe-inline';",
+ "object-src 'none'");
+
+ final String monitor = "default-src 'self'; script-src 'self'; connect-src 'self'; frame-src 'self'; object-src 'none'";
+
+ final PolicyListInOrigin list = ofSerializedIgnored(List.of(app, paymentsOnly, monitor));
+
+ assertTrue(list.allowsScriptFromSource(uri("https://example.com/checkout.js")));
+ assertFalse(list.allowsScriptFromSource(uri("https://js.stripe.com/v3/")));
+ assertFalse(list.allowsScriptFromSource(uri("https://checkout.paypal.com/js")));
+ assertFalse(list.allowsFrameFromSource(uri("https://js.stripe.com/v3/")));
+ assertFalse(list.allowsConnection(uri("https://api.stripe.com/v1/tokens")));
+ assertTrue(list.allowsConnection(uri("https://example.com/api/order")));
+ // monitor falls back to default-src 'self' for style → no 'unsafe-inline'
+ assertFalse(list.allowsUnsafeInlineStyle());
+ assertFalse(list.allowsUnsafeInlineScript());
+ }
+
+ /**
+ * Host / scheme matching is ASCII case-insensitive across separately parsed policies
+ * (CSP3 host-part / scheme-part match). Differently cased hosts in two policies still
+ * share an intersection — Path A never merges strings, so Salvation #120-style case
+ * loss on intersect does not apply.
+ */
+ @Test
+ public void hostCaseInsensitiveAcrossPolicies() {
+ final PolicyListInOrigin list = ofSerializedIgnored(List.of(
+ "img-src https://CDN.Example.COM",
+ "img-src HTTPS://cdn.example.com"));
+
+ assertTrue(list.allowsImageFromSource(uri("https://cdn.example.com/a.png")));
+ assertTrue(list.allowsImageFromSource(uri("https://CDN.EXAMPLE.COM/a.png")));
+ assertTrue(list.allowsImageFromSource(uri("HTTPS://CdN.ExAmPlE.cOm/a.png")));
+ assertFalse(list.allowsImageFromSource(uri("https://other.example.com/a.png")));
+ }
+
+ /**
+ * Path matching is case-sensitive. Same path casing in both policies allows that path;
+ * mismatched path casing across policies yields empty intersection for those URLs.
+ */
+ @Test
+ public void pathCaseSensitiveAcrossPolicies() {
+ final PolicyListInOrigin sameCase = ofSerializedIgnored(List.of(
+ "script-src https://cdn.example.com/App/",
+ "script-src https://cdn.example.com/App/ https://cdn.example.com/other/"));
+
+ assertTrue(sameCase.allowsScriptFromSource(uri("https://cdn.example.com/App/x.js")));
+ assertFalse(sameCase.allowsScriptFromSource(uri("https://cdn.example.com/app/x.js")));
+ assertFalse(sameCase.allowsScriptFromSource(uri("https://cdn.example.com/other/x.js")));
+
+ // /App/ ∩ /app/ — no URL path satisfies both (case-sensitive)
+ final PolicyListInOrigin mismatchedCase = ofSerializedIgnored(List.of(
+ "script-src https://cdn.example.com/App/",
+ "script-src https://cdn.example.com/app/"));
+
+ assertFalse(mismatchedCase.allowsScriptFromSource(uri("https://cdn.example.com/App/x.js")));
+ assertFalse(mismatchedCase.allowsScriptFromSource(uri("https://cdn.example.com/app/x.js")));
+ }
+
+ /**
+ * Invalid {@code 'none' host} in one directive: parse reports an error, but the host
+ * expression still participates in matching (same as single-policy query behavior).
+ * Under AND, a pure {@code 'none'} sibling policy still blocks.
+ */
+ @Test
+ public void noneCombinedWithHostUnderAnd() {
+ // Garbage directive alone (via PolicyList of one) still allows the host
+ final PolicyListInOrigin garbageAlone = ofSerializedIgnored(List.of(
+ "script-src 'none' https://cdn.example.com"));
+ assertTrue(garbageAlone.allowsScriptFromSource(uri("https://cdn.example.com/a.js")));
+ assertFalse(garbageAlone.allowsScriptFromSource(uri("https://other.example.com/a.js")));
+
+ // Overlap with a second policy that also lists the host → allow
+ final PolicyListInOrigin overlap = ofSerializedIgnored(List.of(
+ "script-src 'none' https://cdn.example.com",
+ "script-src https://cdn.example.com https://other.example.com"));
+ assertTrue(overlap.allowsScriptFromSource(uri("https://cdn.example.com/a.js")));
+ assertFalse(overlap.allowsScriptFromSource(uri("https://other.example.com/a.js")));
+
+ // Pure 'none' in the other policy → block everything
+ final PolicyListInOrigin noneSibling = ofSerializedIgnored(List.of(
+ "script-src 'none' https://cdn.example.com",
+ "script-src 'none'"));
+ assertFalse(noneSibling.allowsScriptFromSource(uri("https://cdn.example.com/a.js")));
+ }
+
+ /**
+ * Nonce present in both policies allows inline; second policy without the nonce blocks
+ * even if the first would allow via nonce.
+ */
+ @Test
+ public void nonceMustSatisfyEveryPolicy() {
+ final PolicyList both = PolicyList.ofSerialized(
+ List.of(
+ "script-src 'nonce-abc123' 'strict-dynamic'",
+ "script-src 'nonce-abc123' https://cdn.example.com"),
+ PolicyListErrorConsumer.ignored);
+ assertTrue(both.allowsInlineScript(
+ Optional.of("abc123"), Optional.of("console.log(1)"), Optional.of(false)));
+
+ final PolicyList mismatched = PolicyList.ofSerialized(
+ List.of(
+ "script-src 'nonce-abc123'",
+ "script-src 'nonce-other'"),
+ PolicyListErrorConsumer.ignored);
+ assertFalse(mismatched.allowsInlineScript(
+ Optional.of("abc123"), Optional.of("console.log(1)"), Optional.of(false)));
+ assertFalse(mismatched.allowsInlineScript(
+ Optional.of("other"), Optional.of("console.log(1)"), Optional.of(false)));
+ }
+
+ private static PolicyListInOrigin ofSerialized(final List values) {
+ return new PolicyListInOrigin(
+ PolicyList.ofSerialized(values, ThrowIfPolicyListError), ORIGIN);
+ }
+
+ private static PolicyListInOrigin ofSerializedIgnored(final List values) {
+ return new PolicyListInOrigin(
+ PolicyList.ofSerialized(values, PolicyListErrorConsumer.ignored), ORIGIN);
+ }
+
+ private static URLWithScheme uri(final String url) {
+ return URI.parseURI(url).orElseThrow();
+ }
+}
diff --git a/src/test/java/org/htmlunit/csp/QueryingTest.java b/src/test/java/org/htmlunit/csp/QueryingTest.java
index dd4b8e2..fa4deb5 100644
--- a/src/test/java/org/htmlunit/csp/QueryingTest.java
+++ b/src/test/java/org/htmlunit/csp/QueryingTest.java
@@ -1473,6 +1473,39 @@ public void sandbox() {
assertTrue(p.allowsExternalScript(Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty()));
}
+ /**
+ * Meta-delivered {@code sandbox} / {@code frame-ancestors} stay in the model for
+ * analysis but do not affect {@code allows*} (HTML strips them before enforce).
+ */
+ @Test
+ public void metaInvalidDirectivesIgnoredForQuery() {
+ final Policy sandboxMeta = Policy.parseSerializedCSP(
+ "sandbox", PolicyErrorConsumer.ignored, true);
+ assertTrue(sandboxMeta.deliveredViaMeta());
+ assertTrue(sandboxMeta.sandbox().isPresent());
+ assertTrue(sandboxMeta.allowsScriptAsAttribute(Optional.empty()));
+ assertTrue(sandboxMeta.allowsInlineScript(Optional.empty(), Optional.empty(), Optional.empty()));
+ assertTrue(sandboxMeta.allowsExternalScript(
+ Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty()));
+ assertTrue(sandboxMeta.allowsFormAction(
+ Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty()));
+
+ final Policy faMeta = Policy.parseSerializedCSP(
+ "frame-ancestors 'none'", PolicyErrorConsumer.ignored, true);
+ assertTrue(faMeta.frameAncestors().isPresent());
+ assertTrue(faMeta.allowsFrameAncestor(
+ Optional.of(URI.parseURI("https://evil.example/").orElseThrow()), Optional.empty()));
+
+ // Header delivery still enforces
+ final Policy sandboxHeader = Policy.parseSerializedCSP(
+ "sandbox", PolicyErrorConsumer.ignored, false);
+ assertFalse(sandboxHeader.allowsInlineScript(Optional.empty(), Optional.empty(), Optional.empty()));
+ final Policy faHeader = Policy.parseSerializedCSP(
+ "frame-ancestors 'none'", PolicyErrorConsumer.ignored, false);
+ assertFalse(faHeader.allowsFrameAncestor(
+ Optional.of(URI.parseURI("https://evil.example/").orElseThrow()), Optional.empty()));
+ }
+
private Policy parse(final String policy) {
return Policy.parseSerializedCSP(policy, ThrowIfPolicyError);
}
From 12f760cc02ef1b42b69473a3b45816ba996467b1 Mon Sep 17 00:00:00 2001
From: kingthorin
Date: Thu, 23 Jul 2026 20:36:44 -0400
Subject: [PATCH 2/2] refactor: share CspQueries for multi-policy API
Keep Policy/PolicyList query surfaces in sync via one interface and
one origin wrapper so new allows* methods are not copy-pasted four ways.
---
AGENTS.md | 5 +-
README.md | 4 +-
.../java/org/htmlunit/csp/CspQueries.java | 281 ++++++++++++++++++
.../org/htmlunit/csp/CspQueriesInOrigin.java | 257 ++++++++++++++++
src/main/java/org/htmlunit/csp/Policy.java | 3 +-
.../java/org/htmlunit/csp/PolicyInOrigin.java | 262 +---------------
.../java/org/htmlunit/csp/PolicyList.java | 259 ++++------------
.../org/htmlunit/csp/PolicyListInOrigin.java | 200 +------------
.../htmlunit/csp/PolicyListQueryingTest.java | 15 +
9 files changed, 642 insertions(+), 644 deletions(-)
create mode 100644 src/main/java/org/htmlunit/csp/CspQueries.java
create mode 100644 src/main/java/org/htmlunit/csp/CspQueriesInOrigin.java
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 9e6de6a..1df5700 100644
--- a/README.md
+++ b/README.md
@@ -91,7 +91,9 @@ Those three directives remain available via getters for analysis, but `allows*`
### Multiple Policies
-Browsers enforce every CSP delivered via headers and/or `` together: a resource is allowed only if **every** policy allows it ([CSP3 multiple policies](https://w3c.github.io/webappsec-csp/#multiple-policies)). This library does not merge policies into one string; it ANDs query results on `PolicyList`.
+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)
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 nonce,
+ Optional integrity,
+ Optional extends URLWithScheme> scriptUrl,
+ Optional parserInserted,
+ Optional extends URLWithScheme> origin);
+
+ /**
+ * Returns whether an inline script is allowed.
+ *
+ * @param nonce script nonce, if any
+ * @param source inline script text, if known
+ * @param parserInserted whether the script is parser-inserted
+ * @return {@code true} if allowed under this query target's rules
+ * @see Policy#allowsInlineScript
+ */
+ boolean allowsInlineScript(Optional nonce,
+ Optional source, Optional parserInserted);
+
+ /**
+ * Returns whether a script event-handler attribute is allowed.
+ *
+ * @param source attribute text, if known
+ * @return {@code true} if allowed under this query target's rules
+ * @see Policy#allowsScriptAsAttribute
+ */
+ boolean allowsScriptAsAttribute(Optional source);
+
+ /**
+ * Returns whether eval is allowed.
+ *
+ * @return {@code true} if allowed under this query target's rules
+ * @see Policy#allowsEval
+ */
+ boolean allowsEval();
+
+ /**
+ * Returns whether navigation is allowed.
+ *
+ * @param to navigation target URL, if known
+ * @param redirected whether the navigation is a redirect
+ * @param redirectedTo final URL after redirect, if known
+ * @param origin protected-resource origin, if known
+ * @return {@code true} if allowed under this query target's rules
+ * @see Policy#allowsNavigation
+ */
+ boolean allowsNavigation(
+ Optional extends URLWithScheme> to,
+ Optional redirected,
+ Optional extends URLWithScheme> redirectedTo,
+ Optional extends URLWithScheme> origin);
+
+ /**
+ * Returns whether a form action is allowed.
+ *
+ * @param to form action URL, if known
+ * @param redirected whether the submission redirects
+ * @param redirectedTo final URL after redirect, if known
+ * @param origin protected-resource origin, if known
+ * @return {@code true} if allowed under this query target's rules
+ * @see Policy#allowsFormAction
+ */
+ boolean allowsFormAction(
+ Optional extends URLWithScheme> to,
+ Optional redirected,
+ Optional extends URLWithScheme> redirectedTo,
+ Optional extends URLWithScheme> origin);
+
+ /**
+ * Returns whether a {@code javascript:} URL navigation is allowed.
+ *
+ * @param source JavaScript after the {@code javascript:} prefix, if known
+ * @param origin protected-resource origin, if known
+ * @return {@code true} if allowed under this query target's rules
+ * @see Policy#allowsJavascriptUrlNavigation
+ */
+ boolean allowsJavascriptUrlNavigation(
+ Optional source,
+ Optional extends URLWithScheme> origin);
+
+ /**
+ * Returns whether an external stylesheet is allowed.
+ *
+ * @param nonce style nonce, if any
+ * @param styleUrl stylesheet URL, if known
+ * @param origin protected-resource origin, if known
+ * @return {@code true} if allowed under this query target's rules
+ * @see Policy#allowsExternalStyle
+ */
+ boolean allowsExternalStyle(
+ Optional nonce,
+ Optional extends URLWithScheme> styleUrl,
+ Optional extends URLWithScheme> origin);
+
+ /**
+ * Returns whether an inline style is allowed.
+ *
+ * @param nonce style nonce, if any
+ * @param source inline style text, if known
+ * @return {@code true} if allowed under this query target's rules
+ * @see Policy#allowsInlineStyle
+ */
+ boolean allowsInlineStyle(Optional nonce, Optional source);
+
+ /**
+ * Returns whether a style attribute is allowed.
+ *
+ * @param source attribute text, if known
+ * @return {@code true} if allowed under this query target's rules
+ * @see Policy#allowsStyleAsAttribute
+ */
+ boolean allowsStyleAsAttribute(Optional source);
+
+ /**
+ * Returns whether a frame is allowed.
+ *
+ * @param source framed resource URL, if known
+ * @param origin protected-resource origin, if known
+ * @return {@code true} if allowed under this query target's rules
+ * @see Policy#allowsFrame
+ */
+ boolean allowsFrame(Optional extends URLWithScheme> source,
+ Optional extends URLWithScheme> origin);
+
+ /**
+ * Returns whether a frame ancestor is allowed.
+ *
+ * @param source ancestor frame URL, if known
+ * @param origin protected-resource origin, if known
+ * @return {@code true} if allowed under this query target's rules
+ * @see Policy#allowsFrameAncestor
+ */
+ boolean allowsFrameAncestor(Optional extends URLWithScheme> source,
+ Optional extends URLWithScheme> origin);
+
+ /**
+ * Returns whether a connection is allowed.
+ *
+ * @param source connection URL, if known
+ * @param origin protected-resource origin, if known
+ * @return {@code true} if allowed under this query target's rules
+ * @see Policy#allowsConnection
+ */
+ boolean allowsConnection(Optional extends URLWithScheme> source,
+ Optional extends URLWithScheme> origin);
+
+ /**
+ * Returns whether a font is allowed.
+ *
+ * @param source font URL, if known
+ * @param origin protected-resource origin, if known
+ * @return {@code true} if allowed under this query target's rules
+ * @see Policy#allowsFont
+ */
+ boolean allowsFont(Optional extends URLWithScheme> source,
+ Optional extends URLWithScheme> origin);
+
+ /**
+ * Returns whether an image is allowed.
+ *
+ * @param source image URL, if known
+ * @param origin protected-resource origin, if known
+ * @return {@code true} if allowed under this query target's rules
+ * @see Policy#allowsImage
+ */
+ boolean allowsImage(Optional extends URLWithScheme> source,
+ Optional extends URLWithScheme> origin);
+
+ /**
+ * Returns whether an application manifest is allowed.
+ *
+ * @param source manifest URL, if known
+ * @param origin protected-resource origin, if known
+ * @return {@code true} if allowed under this query target's rules
+ * @see Policy#allowsApplicationManifest
+ */
+ boolean allowsApplicationManifest(Optional extends URLWithScheme> source,
+ Optional extends URLWithScheme> origin);
+
+ /**
+ * Returns whether media is allowed.
+ *
+ * @param source media URL, if known
+ * @param origin protected-resource origin, if known
+ * @return {@code true} if allowed under this query target's rules
+ * @see Policy#allowsMedia
+ */
+ boolean allowsMedia(Optional extends URLWithScheme> source,
+ Optional extends URLWithScheme> origin);
+
+ /**
+ * Returns whether an object is allowed.
+ *
+ * @param source object URL, if known
+ * @param origin protected-resource origin, if known
+ * @return {@code true} if allowed under this query target's rules
+ * @see Policy#allowsObject
+ */
+ boolean allowsObject(Optional extends URLWithScheme> source,
+ Optional extends URLWithScheme> origin);
+
+ /**
+ * Returns whether a prefetch is allowed.
+ *
+ * @param source prefetch URL, if known
+ * @param origin protected-resource origin, if known
+ * @return {@code true} if allowed under this query target's rules
+ * @see Policy#allowsPrefetch
+ */
+ boolean allowsPrefetch(Optional extends URLWithScheme> source,
+ Optional extends URLWithScheme> origin);
+
+ /**
+ * Returns whether a worker is allowed.
+ *
+ * @param source worker URL, if known
+ * @param origin protected-resource origin, if known
+ * @return {@code true} if allowed under this query target's rules
+ * @see Policy#allowsWorker
+ */
+ boolean allowsWorker(Optional extends URLWithScheme> source,
+ Optional extends URLWithScheme> origin);
+
+ /**
+ * Returns whether a plugin type is allowed.
+ *
+ * @param mediaType plugin media type, if known
+ * @return {@code true} if allowed under this query target's rules
+ * @see Policy#allowsPlugin
+ */
+ boolean allowsPlugin(Optional extends MediaType> mediaType);
+}
diff --git a/src/main/java/org/htmlunit/csp/CspQueriesInOrigin.java b/src/main/java/org/htmlunit/csp/CspQueriesInOrigin.java
new file mode 100644
index 0000000..ba81a33
--- /dev/null
+++ b/src/main/java/org/htmlunit/csp/CspQueriesInOrigin.java
@@ -0,0 +1,257 @@
+/*
+ * 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.Objects;
+import java.util.Optional;
+
+import org.htmlunit.csp.url.URLWithScheme;
+
+/**
+ * Convenience wrapper that pairs a {@link CspQueries} target with an
+ * {@link URLWithScheme origin}, supplying simplified query methods that
+ * fill in the origin and leave unused parameters empty.
+ *
+ * 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 a728cb6..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 `, ,`)
diff --git a/src/main/java/org/htmlunit/csp/PolicyInOrigin.java b/src/main/java/org/htmlunit/csp/PolicyInOrigin.java
index ca43072..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,244 +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 eval and similar string-to-code mechanisms.
- *
- * @return {@code true} if the policy allows eval
- * @see Policy#allowsEval
- * @since 5.4.0
- */
- public boolean allowsEval() {
- return policy_.allowsEval();
- }
-
- /**
- * 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 28b6859..8d2b65d 100644
--- a/src/main/java/org/htmlunit/csp/PolicyList.java
+++ b/src/main/java/org/htmlunit/csp/PolicyList.java
@@ -17,6 +17,7 @@
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;
@@ -36,18 +37,22 @@
* 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, PolicyListErrorConsumer)}
* or {@link #ofSerialized(List, PolicyListErrorConsumer)}.
* Empty policies (those with no directives) are omitted during parsing.
- * An empty list (no policies) is treated as unrestricted: all {@code allows*}
- * methods return {@code true}.
*
*
* @author Ronald Brill
+ * @see CspQueries
* @see Policy#parseSerializedCSPList(String, PolicyListErrorConsumer)
*/
-public class PolicyList {
+public class PolicyList implements CspQueries {
private final List policies_;
/**
@@ -122,291 +127,153 @@ public String toString() {
return out.toString();
}
- // High-level querying: AND across all member policies
-
/**
- * Whether every member policy allows the external script.
- * @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 every member allows
- * @see Policy#allowsExternalScript
- * @since 5.4.0
+ * 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 check) {
+ for (final Policy policy : policies_) {
+ if (!check.test(policy)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ // High-level querying: AND across all member policies
+
+ @Override
public boolean allowsExternalScript(
final Optional nonce,
final Optional integrity,
final Optional extends URLWithScheme> scriptUrl,
final Optional parserInserted,
final Optional extends URLWithScheme> origin) {
- return policies_.stream().allMatch(p ->
+ return allAllow(p ->
p.allowsExternalScript(nonce, integrity, scriptUrl, parserInserted, origin));
}
- /**
- * Whether every member policy allows the inline script.
- * @param nonce script nonce, if any
- * @param source inline script text, if known
- * @param parserInserted whether the script is parser-inserted
- * @return {@code true} if every member allows
- * @see Policy#allowsInlineScript
- * @since 5.4.0
- */
+ @Override
public boolean allowsInlineScript(final Optional nonce,
final Optional source, final Optional parserInserted) {
- return policies_.stream().allMatch(p -> p.allowsInlineScript(nonce, source, parserInserted));
+ return allAllow(p -> p.allowsInlineScript(nonce, source, parserInserted));
}
- /**
- * Whether every member policy allows the script event-handler attribute.
- * @param source attribute text, if known
- * @return {@code true} if every member allows
- * @see Policy#allowsScriptAsAttribute
- * @since 5.4.0
- */
+ @Override
public boolean allowsScriptAsAttribute(final Optional source) {
- return policies_.stream().allMatch(p -> p.allowsScriptAsAttribute(source));
+ return allAllow(p -> p.allowsScriptAsAttribute(source));
}
- /**
- * Whether every member policy allows eval.
- * @return {@code true} if every member allows
- * @see Policy#allowsEval
- * @since 5.4.0
- */
+ @Override
public boolean allowsEval() {
- return policies_.stream().allMatch(Policy::allowsEval);
+ return allAllow(Policy::allowsEval);
}
- /**
- * Whether every member policy allows the navigation.
- * @param to navigation target URL, if known
- * @param redirected whether the navigation is a redirect
- * @param redirectedTo final URL after redirect, if known
- * @param origin protected-resource origin, if known
- * @return {@code true} if every member allows
- * @see Policy#allowsNavigation
- * @since 5.4.0
- */
+ @Override
public boolean allowsNavigation(
final Optional extends URLWithScheme> to,
final Optional redirected,
final Optional extends URLWithScheme> redirectedTo,
final Optional extends URLWithScheme> origin) {
- return policies_.stream().allMatch(p -> p.allowsNavigation(to, redirected, redirectedTo, origin));
+ return allAllow(p -> p.allowsNavigation(to, redirected, redirectedTo, origin));
}
- /**
- * Whether every member policy allows the form action.
- * @param to form action URL, if known
- * @param redirected whether the submission redirects
- * @param redirectedTo final URL after redirect, if known
- * @param origin protected-resource origin, if known
- * @return {@code true} if every member allows
- * @see Policy#allowsFormAction
- * @since 5.4.0
- */
+ @Override
public boolean allowsFormAction(
final Optional extends URLWithScheme> to,
final Optional redirected,
final Optional extends URLWithScheme> redirectedTo,
final Optional extends URLWithScheme> origin) {
- return policies_.stream().allMatch(p -> p.allowsFormAction(to, redirected, redirectedTo, origin));
+ return allAllow(p -> p.allowsFormAction(to, redirected, redirectedTo, origin));
}
- /**
- * Whether every member policy allows the {@code javascript:} URL navigation.
- * @param source JavaScript after the {@code javascript:} prefix, if known
- * @param origin protected-resource origin, if known
- * @return {@code true} if every member allows
- * @see Policy#allowsJavascriptUrlNavigation
- * @since 5.4.0
- */
+ @Override
public boolean allowsJavascriptUrlNavigation(
final Optional source,
final Optional extends URLWithScheme> origin) {
- return policies_.stream().allMatch(p -> p.allowsJavascriptUrlNavigation(source, origin));
+ return allAllow(p -> p.allowsJavascriptUrlNavigation(source, origin));
}
- /**
- * Whether every member policy allows the external stylesheet.
- * @param nonce link nonce, if any
- * @param styleUrl stylesheet URL, if known
- * @param origin protected-resource origin, if known
- * @return {@code true} if every member allows
- * @see Policy#allowsExternalStyle
- * @since 5.4.0
- */
+ @Override
public boolean allowsExternalStyle(
final Optional nonce,
final Optional extends URLWithScheme> styleUrl,
final Optional extends URLWithScheme> origin) {
- return policies_.stream().allMatch(p -> p.allowsExternalStyle(nonce, styleUrl, origin));
+ return allAllow(p -> p.allowsExternalStyle(nonce, styleUrl, origin));
}
- /**
- * Whether every member policy allows the inline style.
- * @param nonce style nonce, if any
- * @param source inline style text, if known
- * @return {@code true} if every member allows
- * @see Policy#allowsInlineStyle
- * @since 5.4.0
- */
+ @Override
public boolean allowsInlineStyle(final Optional nonce, final Optional source) {
- return policies_.stream().allMatch(p -> p.allowsInlineStyle(nonce, source));
+ return allAllow(p -> p.allowsInlineStyle(nonce, source));
}
- /**
- * Whether every member policy allows the style attribute.
- * @param source attribute text, if known
- * @return {@code true} if every member allows
- * @see Policy#allowsStyleAsAttribute
- * @since 5.4.0
- */
+ @Override
public boolean allowsStyleAsAttribute(final Optional source) {
- return policies_.stream().allMatch(p -> p.allowsStyleAsAttribute(source));
+ return allAllow(p -> p.allowsStyleAsAttribute(source));
}
- /**
- * Whether every member policy allows the frame.
- * @param source framed resource URL, if known
- * @param origin protected-resource origin, if known
- * @return {@code true} if every member allows
- * @see Policy#allowsFrame
- * @since 5.4.0
- */
+ @Override
public boolean allowsFrame(final Optional extends URLWithScheme> source,
final Optional extends URLWithScheme> origin) {
- return policies_.stream().allMatch(p -> p.allowsFrame(source, origin));
+ return allAllow(p -> p.allowsFrame(source, origin));
}
- /**
- * Whether every member policy allows the frame ancestor.
- * @param source ancestor frame URL, if known
- * @param origin protected-resource origin, if known
- * @return {@code true} if every member allows
- * @see Policy#allowsFrameAncestor
- * @since 5.4.0
- */
+ @Override
public boolean allowsFrameAncestor(final Optional extends URLWithScheme> source,
final Optional extends URLWithScheme> origin) {
- return policies_.stream().allMatch(p -> p.allowsFrameAncestor(source, origin));
+ return allAllow(p -> p.allowsFrameAncestor(source, origin));
}
- /**
- * Whether every member policy allows the connection.
- * @param source connection URL, if known
- * @param origin protected-resource origin, if known
- * @return {@code true} if every member allows
- * @see Policy#allowsConnection
- * @since 5.4.0
- */
+ @Override
public boolean allowsConnection(final Optional extends URLWithScheme> source,
final Optional extends URLWithScheme> origin) {
- return policies_.stream().allMatch(p -> p.allowsConnection(source, origin));
+ return allAllow(p -> p.allowsConnection(source, origin));
}
- /**
- * Whether every member policy allows the font.
- * @param source font URL, if known
- * @param origin protected-resource origin, if known
- * @return {@code true} if every member allows
- * @see Policy#allowsFont
- * @since 5.4.0
- */
+ @Override
public boolean allowsFont(final Optional extends URLWithScheme> source,
final Optional extends URLWithScheme> origin) {
- return policies_.stream().allMatch(p -> p.allowsFont(source, origin));
+ return allAllow(p -> p.allowsFont(source, origin));
}
- /**
- * Whether every member policy allows the image.
- * @param source image URL, if known
- * @param origin protected-resource origin, if known
- * @return {@code true} if every member allows
- * @see Policy#allowsImage
- * @since 5.4.0
- */
+ @Override
public boolean allowsImage(final Optional extends URLWithScheme> source,
final Optional extends URLWithScheme> origin) {
- return policies_.stream().allMatch(p -> p.allowsImage(source, origin));
+ return allAllow(p -> p.allowsImage(source, origin));
}
- /**
- * Whether every member policy allows the application manifest.
- * @param source manifest URL, if known
- * @param origin protected-resource origin, if known
- * @return {@code true} if every member allows
- * @see Policy#allowsApplicationManifest
- * @since 5.4.0
- */
+ @Override
public boolean allowsApplicationManifest(final Optional extends URLWithScheme> source,
final Optional extends URLWithScheme> origin) {
- return policies_.stream().allMatch(p -> p.allowsApplicationManifest(source, origin));
+ return allAllow(p -> p.allowsApplicationManifest(source, origin));
}
- /**
- * Whether every member policy allows the media.
- * @param source media URL, if known
- * @param origin protected-resource origin, if known
- * @return {@code true} if every member allows
- * @see Policy#allowsMedia
- * @since 5.4.0
- */
+ @Override
public boolean allowsMedia(final Optional extends URLWithScheme> source,
final Optional extends URLWithScheme> origin) {
- return policies_.stream().allMatch(p -> p.allowsMedia(source, origin));
+ return allAllow(p -> p.allowsMedia(source, origin));
}
- /**
- * Whether every member policy allows the object.
- * @param source object URL, if known
- * @param origin protected-resource origin, if known
- * @return {@code true} if every member allows
- * @see Policy#allowsObject
- * @since 5.4.0
- */
+ @Override
public boolean allowsObject(final Optional extends URLWithScheme> source,
final Optional extends URLWithScheme> origin) {
- return policies_.stream().allMatch(p -> p.allowsObject(source, origin));
+ return allAllow(p -> p.allowsObject(source, origin));
}
- /**
- * Whether every member policy allows the prefetch.
- * @param source prefetch URL, if known
- * @param origin protected-resource origin, if known
- * @return {@code true} if every member allows
- * @see Policy#allowsPrefetch
- * @since 5.4.0
- */
+ @Override
public boolean allowsPrefetch(final Optional extends URLWithScheme> source,
final Optional extends URLWithScheme> origin) {
- return policies_.stream().allMatch(p -> p.allowsPrefetch(source, origin));
+ return allAllow(p -> p.allowsPrefetch(source, origin));
}
- /**
- * Whether every member policy allows the worker.
- * @param source worker URL, if known
- * @param origin protected-resource origin, if known
- * @return {@code true} if every member allows
- * @see Policy#allowsWorker
- * @since 5.4.0
- */
+ @Override
public boolean allowsWorker(final Optional extends URLWithScheme> source,
final Optional extends URLWithScheme> origin) {
- return policies_.stream().allMatch(p -> p.allowsWorker(source, origin));
+ return allAllow(p -> p.allowsWorker(source, origin));
}
- /**
- * Whether every member policy allows the plugin type.
- * @param mediaType plugin media type, if known
- * @return {@code true} if every member allows
- * @see Policy#allowsPlugin
- * @since 5.4.0
- */
+ @Override
public boolean allowsPlugin(final Optional extends MediaType> mediaType) {
- return policies_.stream().allMatch(p -> p.allowsPlugin(mediaType));
+ return allAllow(p -> p.allowsPlugin(mediaType));
}
}
diff --git a/src/main/java/org/htmlunit/csp/PolicyListInOrigin.java b/src/main/java/org/htmlunit/csp/PolicyListInOrigin.java
index 220afba..134da16 100644
--- a/src/main/java/org/htmlunit/csp/PolicyListInOrigin.java
+++ b/src/main/java/org/htmlunit/csp/PolicyListInOrigin.java
@@ -14,28 +14,20 @@
*/
package org.htmlunit.csp;
-import java.util.Optional;
-
import org.htmlunit.csp.url.URLWithScheme;
/**
* A convenience wrapper that pairs a {@link PolicyList} with its
- * {@link URLWithScheme origin}, providing simplified query methods that
- * automatically supply the origin to the underlying list checks.
- *
- * Each {@code allows*} method delegates to the corresponding method on
- * {@link PolicyList}, which returns {@code true} only when every member
- * {@link Policy} allows the resource.
- *
+ * {@link URLWithScheme origin}. Query methods are defined on
+ * {@link CspQueriesInOrigin}; AND semantics come from {@link PolicyList}.
*
* @author Ronald Brill
+ * @see CspQueriesInOrigin
* @see PolicyInOrigin
* @see PolicyList
* @since 5.4.0
*/
-public class PolicyListInOrigin {
- private final PolicyList policyList_;
- private final URLWithScheme origin_;
+public class PolicyListInOrigin extends CspQueriesInOrigin {
/**
* Ctor.
@@ -44,8 +36,7 @@ public class PolicyListInOrigin {
* @param origin the origin of the protected resource
*/
public PolicyListInOrigin(final PolicyList policyList, final URLWithScheme origin) {
- policyList_ = policyList;
- origin_ = origin;
+ super(policyList, origin);
}
/**
@@ -54,185 +45,6 @@ public PolicyListInOrigin(final PolicyList policyList, final URLWithScheme origi
* @return the policy list associated with this origin-bound wrapper
*/
public PolicyList getPolicyList() {
- return policyList_;
- }
-
- /**
- * Returns the underlying origin.
- *
- * @return the origin of the protected resource
- */
- public URLWithScheme getOrigin() {
- return origin_;
- }
-
- /**
- * Determines whether every policy allows loading an external script from the given URL.
- *
- * @param url the URL of the external script
- * @return {@code true} if every member policy allows the script from the given source
- */
- public boolean allowsScriptFromSource(final URLWithScheme url) {
- return policyList_.allowsExternalScript(Optional.empty(),
- Optional.empty(), Optional.of(url), Optional.empty(), Optional.of(origin_));
- }
-
- /**
- * Determines whether every policy allows loading an external stylesheet from the given URL.
- *
- * @param url the URL of the external stylesheet
- * @return {@code true} if every member policy allows the style from the given source
- */
- public boolean allowsStyleFromSource(final URLWithScheme url) {
- return policyList_.allowsExternalStyle(Optional.empty(), Optional.of(url), Optional.of(origin_));
- }
-
- /**
- * Determines whether every policy allows loading an image from the given URL.
- *
- * @param url the URL of the image resource
- * @return {@code true} if every member policy allows the image from the given source
- */
- public boolean allowsImageFromSource(final URLWithScheme url) {
- return policyList_.allowsImage(Optional.of(url), Optional.of(origin_));
- }
-
- /**
- * Determines whether every policy allows loading a frame from the given URL.
- *
- * @param url the URL of the framed resource
- * @return {@code true} if every member policy allows the frame from the given source
- */
- public boolean allowsFrameFromSource(final URLWithScheme url) {
- return policyList_.allowsFrame(Optional.of(url), Optional.of(origin_));
- }
-
- /**
- * Determines whether every policy allows loading a worker from the given URL.
- *
- * @param url the URL of the worker script
- * @return {@code true} if every member policy allows the worker from the given source
- */
- public boolean allowsWorkerFromSource(final URLWithScheme url) {
- return policyList_.allowsWorker(Optional.of(url), Optional.of(origin_));
- }
-
- /**
- * Determines whether every policy allows loading a font from the given URL.
- *
- * @param url the URL of the font resource
- * @return {@code true} if every member policy allows the font from the given source
- */
- public boolean allowsFontFromSource(final URLWithScheme url) {
- return policyList_.allowsFont(Optional.of(url), Optional.of(origin_));
- }
-
- /**
- * Determines whether every policy allows loading an object/embed/applet from the given URL.
- *
- * @param url the URL of the object resource
- * @return {@code true} if every member policy allows the object from the given source
- */
- public boolean allowsObjectFromSource(final URLWithScheme url) {
- return policyList_.allowsObject(Optional.of(url), Optional.of(origin_));
- }
-
- /**
- * Determines whether every policy allows loading media (audio/video) from the given URL.
- *
- * @param url the URL of the media resource
- * @return {@code true} if every member policy allows the media from the given source
- */
- public boolean allowsMediaFromSource(final URLWithScheme url) {
- return policyList_.allowsMedia(Optional.of(url), Optional.of(origin_));
- }
-
- /**
- * Determines whether every policy allows loading an application manifest from the given URL.
- *
- * @param url the URL of the manifest resource
- * @return {@code true} if every member policy allows the manifest from the given source
- */
- public boolean allowsManifestFromSource(final URLWithScheme url) {
- return policyList_.allowsApplicationManifest(Optional.of(url), Optional.of(origin_));
- }
-
- /**
- * Determines whether every policy allows a prefetch from the given URL.
- *
- * @param url the URL to prefetch
- * @return {@code true} if every member policy allows the prefetch from the given source
- */
- public boolean allowsPrefetchFromSource(final URLWithScheme url) {
- return policyList_.allowsPrefetch(Optional.of(url), Optional.of(origin_));
- }
-
- /**
- * Determines whether every policy allows any inline script without a nonce or hash.
- *
- * @return {@code true} if every member policy allows inline scripts without specific credentials
- */
- public boolean allowsUnsafeInlineScript() {
- return policyList_.allowsInlineScript(Optional.empty(), Optional.empty(), Optional.empty());
- }
-
- /**
- * Determines whether every policy allows any inline style without a nonce or hash.
- *
- * @return {@code true} if every member policy allows inline styles without specific credentials
- */
- public boolean allowsUnsafeInlineStyle() {
- return policyList_.allowsInlineStyle(Optional.empty(), Optional.empty());
- }
-
- /**
- * Determines whether every policy allows a connection to the given URL.
- *
- * @param url the URL to connect to
- * @return {@code true} if every member policy allows the connection to the given source
- */
- public boolean allowsConnection(final URLWithScheme url) {
- return policyList_.allowsConnection(Optional.of(url), Optional.of(origin_));
- }
-
- /**
- * Determines whether every policy allows navigation to the given URL.
- *
- * @param url the navigation target URL
- * @return {@code true} if every member policy allows navigation to the given URL
- */
- public boolean allowsNavigation(final URLWithScheme url) {
- return policyList_.allowsNavigation(Optional.of(url),
- Optional.empty(), Optional.empty(), Optional.of(origin_));
- }
-
- /**
- * Determines whether every policy allows being framed by the given ancestor URL.
- *
- * @param url the URL of the ancestor frame
- * @return {@code true} if every member policy allows the frame ancestor
- */
- public boolean allowsFrameAncestor(final URLWithScheme url) {
- return policyList_.allowsFrameAncestor(Optional.of(url), Optional.of(origin_));
- }
-
- /**
- * Determines whether every policy allows a form submission to the given URL.
- *
- * @param url the form action target URL
- * @return {@code true} if every member policy allows the form action to the given URL
- */
- public boolean allowsFormAction(final URLWithScheme url) {
- return policyList_.allowsFormAction(Optional.of(url),
- Optional.empty(), Optional.empty(), Optional.of(origin_));
- }
-
- /**
- * Determines whether every policy allows eval.
- *
- * @return {@code true} if every member policy allows eval
- */
- public boolean allowsEval() {
- return policyList_.allowsEval();
+ return (PolicyList) getQueries();
}
}
diff --git a/src/test/java/org/htmlunit/csp/PolicyListQueryingTest.java b/src/test/java/org/htmlunit/csp/PolicyListQueryingTest.java
index e3811bc..e1b498d 100644
--- a/src/test/java/org/htmlunit/csp/PolicyListQueryingTest.java
+++ b/src/test/java/org/htmlunit/csp/PolicyListQueryingTest.java
@@ -139,6 +139,21 @@ public void emptyListAllowsEverything() {
Policy.PolicyErrorConsumer.ignored)));
}
+ @Test
+ public void originWrappersExposeTypedQueries() {
+ final PolicyList list = PolicyList.ofSerialized(
+ List.of("img-src 'self'"), PolicyListErrorConsumer.ignored);
+ final PolicyListInOrigin bound = new PolicyListInOrigin(list, ORIGIN);
+ assertTrue(bound.getQueries() instanceof PolicyList);
+ assertEquals(list, bound.getPolicyList());
+ assertEquals(ORIGIN, bound.getOrigin());
+
+ final Policy policy = list.getPolicies().get(0);
+ final PolicyInOrigin single = new PolicyInOrigin(policy, ORIGIN);
+ assertTrue(single.getQueries() instanceof Policy);
+ assertEquals(policy, single.getPolicy());
+ }
+
@Test
public void ofSerializedParsesCommaSeparatedHeaderValues() {
// One header value that is itself a CSP list, plus another header