diff --git a/src/main/java/org/htmlunit/csp/Policy.java b/src/main/java/org/htmlunit/csp/Policy.java
index cb41815..cc38071 100644
--- a/src/main/java/org/htmlunit/csp/Policy.java
+++ b/src/main/java/org/htmlunit/csp/Policy.java
@@ -33,8 +33,10 @@
import org.htmlunit.csp.directive.HostSourceDirective;
import org.htmlunit.csp.directive.PluginTypesDirective;
import org.htmlunit.csp.directive.ReportUriDirective;
+import org.htmlunit.csp.directive.RequireTrustedTypesForDirective;
import org.htmlunit.csp.directive.SandboxDirective;
import org.htmlunit.csp.directive.SourceExpressionDirective;
+import org.htmlunit.csp.directive.TrustedTypesDirective;
import org.htmlunit.csp.url.GUID;
import org.htmlunit.csp.url.URI;
import org.htmlunit.csp.url.URLWithScheme;
@@ -70,6 +72,8 @@ public final class Policy {
private RFC7230Token reportTo_;
private ReportUriDirective reportUri_;
private SandboxDirective sandbox_;
+ private TrustedTypesDirective trustedTypes_;
+ private RequireTrustedTypesForDirective requireTrustedTypes_;
private boolean upgradeInsecureRequests_;
private final EnumMap fetchDirectives_
@@ -318,6 +322,32 @@ else if (values.size() == 1) {
newDirective = sandboxDirective;
break;
+ case "trusted-types":
+ // https://www.w3.org/TR/trusted-types/
+ final TrustedTypesDirective trustedTypesDirective = new TrustedTypesDirective(
+ values, directiveErrorConsumer);
+ if (trustedTypes_ == null) {
+ trustedTypes_ = trustedTypesDirective;
+ }
+ else {
+ wasDupe = true;
+ }
+ newDirective = trustedTypesDirective;
+ break;
+
+ case "require-trusted-types-for":
+ // https://www.w3.org/TR/trusted-types/#require-trusted-types-for-csp-directive
+ final RequireTrustedTypesForDirective requireTrustedTypesDirective = new RequireTrustedTypesForDirective(
+ values, directiveErrorConsumer);
+ if (requireTrustedTypes_ == null) {
+ requireTrustedTypes_ = requireTrustedTypesDirective;
+ }
+ else {
+ wasDupe = true;
+ }
+ newDirective = requireTrustedTypesDirective;
+ break;
+
case "upgrade-insecure-requests":
// https://www.w3.org/TR/upgrade-insecure-requests/#delivery
if (upgradeInsecureRequests_) {
diff --git a/src/main/java/org/htmlunit/csp/directive/RequireTrustedTypesForDirective.java b/src/main/java/org/htmlunit/csp/directive/RequireTrustedTypesForDirective.java
new file mode 100644
index 0000000..c1a3a6a
--- /dev/null
+++ b/src/main/java/org/htmlunit/csp/directive/RequireTrustedTypesForDirective.java
@@ -0,0 +1,37 @@
+/*
+ * 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.directive;
+
+import org.htmlunit.csp.Directive;
+import org.htmlunit.csp.Policy;
+import java.util.List;
+
+/**
+ * Directive implementation for `require-trusted-types-for`.
+ */
+public class RequireTrustedTypesForDirective extends Directive {
+
+ public RequireTrustedTypesForDirective(final List values, final DirectiveErrorConsumer errors) {
+ super(values);
+ if (!values.isEmpty()) {
+ for (int i = 0; i < values.size(); i++) {
+ String value = values.get(i);
+ if (!"script".equalsIgnoreCase(value)) {
+ errors.add(Policy.Severity.Error, "`require-trusted-types-for` only accepts 'script' as a value.", i);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/htmlunit/csp/directive/TrustedTypesDirective.java b/src/main/java/org/htmlunit/csp/directive/TrustedTypesDirective.java
new file mode 100644
index 0000000..17f9ed4
--- /dev/null
+++ b/src/main/java/org/htmlunit/csp/directive/TrustedTypesDirective.java
@@ -0,0 +1,109 @@
+/*
+ * 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.directive;
+
+import org.htmlunit.csp.Directive;
+import org.htmlunit.csp.Policy;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * Directive implementation for `trusted-types`.
+ */
+public class TrustedTypesDirective extends Directive {
+
+ private boolean allowDuplicates_;
+ private boolean allowAnyPolicyName_;
+ private Set allowedPolicyNames_ = new HashSet<>();
+
+ public TrustedTypesDirective(final List values, final DirectiveErrorConsumer errors) {
+ super(values);
+ boolean hasNone = values.stream().anyMatch(x -> x.toLowerCase(Locale.ROOT).equals("'none'"));
+
+ if (hasNone && values.size() > 1) {
+ errors.add(Policy.Severity.Error,
+ "Specifying trusted-types 'none' along with other values is invalid", -1);
+ }
+
+ for (int i = 0; i < values.size(); i++) {
+ String value = values.get(i);
+ final String lowercaseValue = value.toLowerCase(Locale.ROOT);
+
+ if ("*".equals(value)) {
+ allowAnyPolicyName_ = true;
+ continue;
+ } else if (value.isBlank()) {
+ errors.add(Policy.Severity.Error, "Empty or whitespace-only policy name is not allowed.", i);
+ continue;
+ }
+
+ // Check for quoted keywords
+ if (value.startsWith("'") && value.endsWith("'") && value.length() > 2) { // Minimum "'x'"
+ String inner = lowercaseValue.substring(1, value.length() - 1);
+ if (inner.equals("allow-duplicates")) {
+ allowDuplicates_ = true;
+ } else if (inner.equals("none")) {
+ // 'none' is ignored here, nNo action needed
+ } else {
+ errors.add(Policy.Severity.Error, "Unknown keyword in trusted-types: " + value, i);
+ }
+ continue;
+ }
+
+ // Validate and add policy name (unquoted)
+ if (isValidPolicyName(value)) {
+ if (!allowedPolicyNames_.add(value)) { // False when attempting to add duplicate
+ // The spec treats the policy name as a JavaScript DOMString identifier, and comparisons are done as exact string matches.
+ // There is no case normalization step.
+ // https://www.w3.org/TR/trusted-types/#should-trusted-type-policy-creation-be-blocked-by-content-security-policy
+ // https://www.w3.org/TR/trusted-types/#create-a-trusted-type-policy
+ // Case-sensitive storage
+ errors.add(Policy.Severity.Warning, "Second attempt to add trusted-types policy: " + value, i);
+ };
+ } else {
+ errors.add(Policy.Severity.Error, "Invalid policy name in trusted-types: " + value
+ + " (must be alphanumeric or -#=_/@.%)", i);
+ }
+ }
+ }
+
+ private static boolean isValidPolicyName(String name) {
+ if (name.isEmpty()) {
+ return false;
+ }
+ for (char c : name.toCharArray()) {
+ if (!Character.isLetterOrDigit(c) && "-#=_/@.%".indexOf(c) == -1) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public boolean isAllowDuplicates() {
+ return allowDuplicates_;
+ }
+
+ public boolean isAllowAnyPolicyName() {
+ return allowAnyPolicyName_;
+ }
+
+ public Set getAllowedPolicyNames() {
+ return Collections.unmodifiableSet(allowedPolicyNames_);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/htmlunit/csp/directive/RequireTrustedTypesForDirectiveTest.java b/src/test/java/org/htmlunit/csp/directive/RequireTrustedTypesForDirectiveTest.java
new file mode 100644
index 0000000..1e7a768
--- /dev/null
+++ b/src/test/java/org/htmlunit/csp/directive/RequireTrustedTypesForDirectiveTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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.directive;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.ArrayList;
+import java.util.List;
+
+class RequireTrustedTypesForDirectiveTest {
+
+ @Test
+ void testValidScriptValue() {
+ // Given
+ List errors = new ArrayList<>();
+ // When
+ // Valid value: 'script'
+ RequireTrustedTypesForDirective directive = new RequireTrustedTypesForDirective(
+ List.of("script"), (severity, message, valueIndex) -> errors.add(message));
+ // Then
+ assertTrue(directive.getValues().contains("script"));
+ assertTrue(errors.isEmpty()); // No errors should be reported
+ }
+
+ @Test
+ void testInvalidValue() {
+ List errors = new ArrayList<>();
+ // Then
+ // Invalid value: 'invalid'
+ RequireTrustedTypesForDirective directive = new RequireTrustedTypesForDirective(
+ List.of("invalid"), (severity, message, valueIndex) -> errors.add(message));
+ // Then
+ assertTrue(directive.getValues().contains("invalid"));
+ assertEquals(1, errors.size()); // One error should be reported
+ assertEquals("`require-trusted-types-for` only accepts 'script' as a value.",
+ errors.get(0));
+ }
+
+ @Test
+ void testInvalidValueAmonstMany() {
+ // Given
+ List errors = new ArrayList<>();
+ // When
+ // Invalid value: 'invalid'
+ RequireTrustedTypesForDirective directive = new RequireTrustedTypesForDirective(
+ List.of("script", "invalid"), (severity, message, valueIndex) -> errors.add(message));
+ // Then
+ assertTrue(directive.getValues().contains("script"));
+ assertTrue(directive.getValues().contains("invalid"));
+ assertEquals(1, errors.size()); // One error should be reported
+ assertEquals("`require-trusted-types-for` only accepts 'script' as a value.",
+ errors.get(0));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/htmlunit/csp/directive/TrustedTypesDirectiveTest.java b/src/test/java/org/htmlunit/csp/directive/TrustedTypesDirectiveTest.java
new file mode 100644
index 0000000..7c7e40b
--- /dev/null
+++ b/src/test/java/org/htmlunit/csp/directive/TrustedTypesDirectiveTest.java
@@ -0,0 +1,165 @@
+/*
+ * 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.directive;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+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.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+class TrustedTypesDirectiveTest {
+
+ private List errors;
+
+ @BeforeEach
+ void setup() {
+ this.errors = new ArrayList<>();
+ }
+
+ @Test
+ void testWildcardAllowed() {
+ // Given / When
+ // Wildcard policy
+ new TrustedTypesDirective(List.of("*"),
+ (severity, message, valueIndex) -> errors.add(message));
+ // Then
+ assertTrue(errors.isEmpty());
+ }
+
+ @Test
+ void testEmptyPolicyNameError() {
+ // Given / When // Then
+ // Whitespace-only policy name
+ assertThrows(IllegalArgumentException.class,
+ () -> new TrustedTypesDirective(List.of("policy1", " "),
+ (severity, message, valueIndex) -> errors.add(message)));
+ }
+
+ @Test
+ void testSimplePolicy() {
+ // Given / When
+ // Ex: trusted-types myPolicy default;
+ new TrustedTypesDirective(List.of("mypolicy", "default"),
+ (severity, message, valueIndex) -> errors.add(message));
+ // Then
+ assertTrue(errors.isEmpty());
+ }
+
+ @Test
+ void testAllowDuplicatesKeyword() {
+ // Given / When
+ TrustedTypesDirective directive = new TrustedTypesDirective(List.of("'allow-duplicates'"),
+ (severity, message, valueIndex) -> errors.add(message));
+ // Then
+ assertTrue(directive.isAllowDuplicates());
+ assertFalse(directive.isAllowAnyPolicyName());
+ assertTrue(directive.getAllowedPolicyNames().isEmpty());
+ assertTrue(errors.isEmpty());
+ }
+
+ @Test
+ void testNoneKeywordAlone() {
+ // Given / When
+ TrustedTypesDirective directive = new TrustedTypesDirective(List.of("'none'"),
+ (severity, message, valueIndex) -> errors.add(message));
+ // Then
+ assertFalse(directive.isAllowAnyPolicyName());
+ assertFalse(directive.isAllowDuplicates());
+ assertTrue(directive.getAllowedPolicyNames().isEmpty());
+ assertTrue(errors.isEmpty());
+ }
+
+ @Test
+ void testNoneKeywordMixedWithPolicies() {
+ // Given / When
+ TrustedTypesDirective directive = new TrustedTypesDirective(List.of("'none'", "my-policy"),
+ (severity, message, valueIndex) -> errors.add(message));
+ // Then
+ assertFalse(directive.isAllowAnyPolicyName());
+ assertFalse(directive.isAllowDuplicates());
+ assertEquals(Set.of("my-policy"), directive.getAllowedPolicyNames());
+ assertEquals("Specifying trusted-types 'none' along with other values is invalid",
+ errors.get(0));
+ }
+
+ @Test
+ void testValidPolicyNames() {
+ // Given / When
+ TrustedTypesDirective directive = new TrustedTypesDirective(
+ List.of("policy1", "policy-2_@.%", "#=/"),
+ (severity, message, valueIndex) -> errors.add(message));
+ // Then
+ assertEquals(Set.of("policy1", "policy-2_@.%", "#=/"), directive.getAllowedPolicyNames());
+ assertTrue(errors.isEmpty());
+ }
+
+ @Test
+ void testCaseSensitivity() {
+ // Given / When
+ TrustedTypesDirective directive = new TrustedTypesDirective(
+ List.of("Policy", "policy", "'Allow-Duplicates'"),
+ (severity, message, valueIndex) -> errors.add(message));
+ // Then
+ // Policy names case-sensitive (both added separately)
+ assertEquals(Set.of("Policy", "policy"), directive.getAllowedPolicyNames());
+ // Keywords case-insensitive for inner content
+ assertTrue(directive.isAllowDuplicates());
+ assertTrue(errors.isEmpty());
+ }
+
+ @Test
+ void testUnknownKeyword() {
+ // Given / When
+ new TrustedTypesDirective(List.of("'unknown'"),
+ (severity, message, valueIndex) -> errors.add(message));
+ // Then
+ assertTrue(errors.size() == 1 && errors.get(0).contains("Unknown keyword"));
+ }
+
+ @Test
+ void testMixedAll() {
+ // Given / When
+ TrustedTypesDirective directive = new TrustedTypesDirective(
+ List.of("*", "'allow-duplicates'", "policy1", "'none'", "invalid!"),
+ (severity, message, valueIndex) -> errors.add(message));
+ // Then
+ assertTrue(directive.isAllowAnyPolicyName());
+ assertTrue(directive.isAllowDuplicates());
+ assertEquals(Set.of("policy1"), directive.getAllowedPolicyNames());
+ assertEquals(2, errors.size());
+ assertTrue(errors
+ .contains("Specifying trusted-types 'none' along with other values is invalid"));
+ assertTrue(errors.contains(
+ "Invalid policy name in trusted-types: invalid! (must be alphanumeric or -#=_/@.%)"));
+ }
+
+ @Test
+ void testDuplicatesWhenNotAllowed() {
+ // Given / When
+ new TrustedTypesDirective(List.of("mypolicy", "mypolicy"),
+ (severity, message, valueIndex) -> errors.add(message));
+ // Then
+ assertFalse(errors.isEmpty());
+ assertEquals(errors.get(0), "Second attempt to add trusted-types policy: mypolicy");
+ }
+}