diff --git a/src/main/java/org/htmlunit/csp/Policy.java b/src/main/java/org/htmlunit/csp/Policy.java
index 1ef00f9..7a73971 100644
--- a/src/main/java/org/htmlunit/csp/Policy.java
+++ b/src/main/java/org/htmlunit/csp/Policy.java
@@ -465,6 +465,16 @@ public Optional trustedTypes() {
return Optional.ofNullable(trustedTypes_);
}
+ /**
+ * Indicates if wildcard policy names are permitted in the trusted-types directive.
+ * When true, any policy name is allowed, which may reduce security.
+ *
+ * @return true if wildcard policy names (*) are permitted, false if not present or not permitted
+ */
+ public boolean allowsWildcardPolicyNames() {
+ return trustedTypes_ != null && trustedTypes_.allowsWildcardPolicyNames();
+ }
+
public Optional requireTrustedTypesFor() {
return Optional.ofNullable(requireTrustedTypesFor_);
}
diff --git a/src/main/java/org/htmlunit/csp/directive/TrustedTypesDirective.java b/src/main/java/org/htmlunit/csp/directive/TrustedTypesDirective.java
index beb277b..f427661 100644
--- a/src/main/java/org/htmlunit/csp/directive/TrustedTypesDirective.java
+++ b/src/main/java/org/htmlunit/csp/directive/TrustedTypesDirective.java
@@ -66,6 +66,8 @@ public TrustedTypesDirective(final List values, final DirectiveErrorCons
case "*":
if (!star_) {
star_ = true;
+ errors.add(Policy.Severity.Warning,
+ "Wildcard policy names (*) permit any policy name, which may reduce security", index);
}
else {
errors.add(Policy.Severity.Warning, "Duplicate wildcard *", index);
@@ -91,11 +93,35 @@ else if (TT_POLICY_NAME_PATTERN.matcher(token).matches()) {
++index;
}
+ // Empty directive validation - if no values were provided, warn
+ if (values.isEmpty()) {
+ errors.add(Policy.Severity.Warning,
+ "Empty trusted-types directive allows all policy names (use '*' or 'none' to be explicit)", -1);
+ }
+
// 'none' must not be combined with other values
if (none_ && (star_ || allowDuplicates_ || !policyNames_.isEmpty())) {
errors.add(Policy.Severity.Error,
"'none' must not be combined with any other trusted-types expression", -1);
}
+
+ // Wildcard makes specific policy names redundant
+ if (star_ && !policyNames_.isEmpty()) {
+ errors.add(Policy.Severity.Warning,
+ "Wildcard (*) permits any policy name, making specific policy names redundant", -1);
+ }
+
+ // 'allow-duplicates' is redundant with wildcard (wildcard already allows everything)
+ if (star_ && allowDuplicates_) {
+ errors.add(Policy.Severity.Warning,
+ "'allow-duplicates' is redundant when wildcard (*) is present", -1);
+ }
+
+ // 'allow-duplicates' without policy names or wildcard has no effect
+ if (allowDuplicates_ && !star_ && policyNames_.isEmpty()) {
+ errors.add(Policy.Severity.Warning,
+ "'allow-duplicates' has no effect without policy names or wildcard", -1);
+ }
}
public boolean none() {
@@ -136,6 +162,16 @@ public boolean star() {
return star_;
}
+ /**
+ * Indicates if wildcard policy names are permitted.
+ * When true, any policy name is allowed, which may reduce security.
+ *
+ * @return true if wildcard policy names (*) are permitted, false otherwise
+ */
+ public boolean allowsWildcardPolicyNames() {
+ return star_;
+ }
+
public void setStar_(final boolean star) {
if (star_ == star) {
return;
diff --git a/src/test/java/org/htmlunit/csp/TrustedTypesTest.java b/src/test/java/org/htmlunit/csp/TrustedTypesTest.java
index 3fca167..583755c 100644
--- a/src/test/java/org/htmlunit/csp/TrustedTypesTest.java
+++ b/src/test/java/org/htmlunit/csp/TrustedTypesTest.java
@@ -51,10 +51,19 @@ public void testTrustedTypesBasic() {
assertEquals(3, tt.getPolicyNames_().size());
// Wildcard
- p = Policy.parseSerializedCSP("trusted-types *", ThrowIfPolicyError);
+ ArrayList observedErrors = new ArrayList<>();
+ Policy.PolicyErrorConsumer consumer = (severity, message, directiveIndex, valueIndex) -> {
+ observedErrors.add(e(severity, message, directiveIndex, valueIndex));
+ };
+ p = Policy.parseSerializedCSP("trusted-types *", consumer);
tt = p.trustedTypes().get();
assertTrue(tt.star());
+ assertTrue(tt.allowsWildcardPolicyNames());
+ assertTrue(p.allowsWildcardPolicyNames());
assertEquals(0, tt.getPolicyNames_().size());
+ assertEquals(1, observedErrors.size());
+ assertEquals(Policy.Severity.Warning, observedErrors.get(0).severity_());
+ assertTrue(observedErrors.get(0).message_().contains("Wildcard policy names"));
// Allow duplicates
p = Policy.parseSerializedCSP("trusted-types myPolicy 'allow-duplicates'", ThrowIfPolicyError);
@@ -63,10 +72,18 @@ public void testTrustedTypesBasic() {
assertEquals(1, tt.getPolicyNames_().size());
// Wildcard with allow-duplicates
- p = Policy.parseSerializedCSP("trusted-types * 'allow-duplicates'", ThrowIfPolicyError);
+ observedErrors.clear();
+ p = Policy.parseSerializedCSP("trusted-types * 'allow-duplicates'", consumer);
tt = p.trustedTypes().get();
assertTrue(tt.star());
+ assertTrue(tt.allowsWildcardPolicyNames());
+ assertTrue(p.allowsWildcardPolicyNames());
assertTrue(tt.allowDuplicates());
+ assertEquals(2, observedErrors.size());
+ assertEquals(Policy.Severity.Warning, observedErrors.get(0).severity_());
+ assertTrue(observedErrors.get(0).message_().contains("Wildcard policy names"));
+ assertEquals(Policy.Severity.Warning, observedErrors.get(1).severity_());
+ assertTrue(observedErrors.get(1).message_().contains("redundant when wildcard"));
// None keyword
p = Policy.parseSerializedCSP("trusted-types 'none'", ThrowIfPolicyError);
@@ -102,10 +119,13 @@ public void testTrustedTypesPolicyNameCharacters() {
public void testTrustedTypesRoundTrips() {
roundTrips("trusted-types myPolicy");
roundTrips("trusted-types one two three");
- roundTrips("trusted-types *");
+ roundTrips("trusted-types *",
+ e(Policy.Severity.Warning, "Wildcard policy names (*) permit any policy name, which may reduce security", 0, 0));
roundTrips("trusted-types 'none'");
roundTrips("trusted-types myPolicy 'allow-duplicates'");
- roundTrips("trusted-types * 'allow-duplicates'");
+ roundTrips("trusted-types * 'allow-duplicates'",
+ e(Policy.Severity.Warning, "Wildcard policy names (*) permit any policy name, which may reduce security", 0, 0),
+ e(Policy.Severity.Warning, "'allow-duplicates' is redundant when wildcard (*) is present", 0, -1));
}
@Test
@@ -113,12 +133,21 @@ public void testTrustedTypesCaseInsensitiveKeywords() {
// Keywords are case-insensitive per ABNF
inTurkey(() -> {
Policy p;
+ ArrayList observedErrors = new ArrayList<>();
+ Policy.PolicyErrorConsumer consumer = (severity, message, directiveIndex, valueIndex) -> {
+ observedErrors.add(e(severity, message, directiveIndex, valueIndex));
+ };
p = Policy.parseSerializedCSP("trusted-types 'NONE'", ThrowIfPolicyError);
assertTrue(p.trustedTypes().get().none());
- p = Policy.parseSerializedCSP("trusted-types 'ALLOW-DUPLICATES'", ThrowIfPolicyError);
+ // 'allow-duplicates' alone now generates a warning, so use consumer instead of ThrowIfPolicyError
+ observedErrors.clear();
+ p = Policy.parseSerializedCSP("trusted-types 'ALLOW-DUPLICATES'", consumer);
assertTrue(p.trustedTypes().get().allowDuplicates());
+ assertEquals(1, observedErrors.size());
+ assertEquals(Policy.Severity.Warning, observedErrors.get(0).severity_());
+ assertTrue(observedErrors.get(0).message_().contains("has no effect without policy names"));
p = Policy.parseSerializedCSP("TRUSTED-TYPES myPolicy", ThrowIfPolicyError);
assertTrue(p.trustedTypes().isPresent());
@@ -175,14 +204,93 @@ public void testTrustedTypesErrors() {
// Duplicate wildcard
roundTrips(
"trusted-types * *",
+ e(Policy.Severity.Warning, "Wildcard policy names (*) permit any policy name, which may reduce security", 0, 0),
e(Policy.Severity.Warning, "Duplicate wildcard *", 0, 1)
);
+ // Policy name with wildcard (wildcard makes policy names redundant)
+ roundTrips(
+ "trusted-types myPolicy *",
+ e(Policy.Severity.Warning, "Wildcard policy names (*) permit any policy name, which may reduce security", 0, 1),
+ e(Policy.Severity.Warning, "Wildcard (*) permits any policy name, making specific policy names redundant", 0, -1)
+ );
+
+ // Multiple policy names with wildcard
+ roundTrips(
+ "trusted-types one two *",
+ e(Policy.Severity.Warning, "Wildcard policy names (*) permit any policy name, which may reduce security", 0, 2),
+ e(Policy.Severity.Warning, "Wildcard (*) permits any policy name, making specific policy names redundant", 0, -1)
+ );
+
// Duplicate directive
roundTrips(
"trusted-types one; trusted-types two",
e(Policy.Severity.Warning, "Duplicate directive trusted-types", 1, -1)
);
+
+ // Empty directive
+ roundTrips(
+ "trusted-types",
+ e(Policy.Severity.Warning, "Empty trusted-types directive allows all policy names (use '*' or 'none' to be explicit)", 0, -1)
+ );
+
+ // 'allow-duplicates' alone (no policy names or wildcard)
+ roundTrips(
+ "trusted-types 'allow-duplicates'",
+ e(Policy.Severity.Warning, "'allow-duplicates' has no effect without policy names or wildcard", 0, -1)
+ );
+
+ // Wildcard with allow-duplicates (redundant)
+ roundTrips(
+ "trusted-types * 'allow-duplicates'",
+ e(Policy.Severity.Warning, "Wildcard policy names (*) permit any policy name, which may reduce security", 0, 0),
+ e(Policy.Severity.Warning, "'allow-duplicates' is redundant when wildcard (*) is present", 0, -1)
+ );
+
+ // Policy names with wildcard and allow-duplicates (multiple redundancies)
+ roundTrips(
+ "trusted-types myPolicy * 'allow-duplicates'",
+ e(Policy.Severity.Warning, "Wildcard policy names (*) permit any policy name, which may reduce security", 0, 1),
+ e(Policy.Severity.Warning, "Wildcard (*) permits any policy name, making specific policy names redundant", 0, -1),
+ e(Policy.Severity.Warning, "'allow-duplicates' is redundant when wildcard (*) is present", 0, -1)
+ );
+
+ // Order independence: wildcard before policy name
+ roundTrips(
+ "trusted-types * myPolicy",
+ e(Policy.Severity.Warning, "Wildcard policy names (*) permit any policy name, which may reduce security", 0, 0),
+ e(Policy.Severity.Warning, "Wildcard (*) permits any policy name, making specific policy names redundant", 0, -1)
+ );
+ }
+
+ @Test
+ public void testTrustedTypesEdgeCases() {
+ Policy p;
+ TrustedTypesDirective tt;
+
+ // Single character policy name
+ p = Policy.parseSerializedCSP("trusted-types a", ThrowIfPolicyError);
+ tt = p.trustedTypes().get();
+ assertEquals(1, tt.getPolicyNames_().size());
+ assertEquals("a", tt.getPolicyNames_().get(0));
+
+ // Policy name with all allowed special characters
+ p = Policy.parseSerializedCSP("trusted-types A-Za-z0-9-#=_/@.%", ThrowIfPolicyError);
+ tt = p.trustedTypes().get();
+ assertEquals(1, tt.getPolicyNames_().size());
+ assertTrue(tt.getPolicyNames_().contains("A-Za-z0-9-#=_/@.%"));
+
+ // Policy name starting with special character
+ p = Policy.parseSerializedCSP("trusted-types -policy", ThrowIfPolicyError);
+ tt = p.trustedTypes().get();
+ assertEquals(1, tt.getPolicyNames_().size());
+ assertEquals("-policy", tt.getPolicyNames_().get(0));
+
+ // Policy name ending with special character
+ p = Policy.parseSerializedCSP("trusted-types policy-", ThrowIfPolicyError);
+ tt = p.trustedTypes().get();
+ assertEquals(1, tt.getPolicyNames_().size());
+ assertEquals("policy-", tt.getPolicyNames_().get(0));
}
// require-trusted-types-for directive tests
@@ -286,6 +394,35 @@ public void testRequireTrustedTypesForManipulation() {
assertTrue(rttf.script());
}
+ @Test
+ public void testAllowsWildcardPolicyNames() {
+ Policy p;
+ TrustedTypesDirective tt;
+
+ // Policy without wildcard
+ p = Policy.parseSerializedCSP("trusted-types myPolicy", ThrowIfPolicyError);
+ assertTrue(p.trustedTypes().isPresent());
+ tt = p.trustedTypes().get();
+ assertFalse(tt.allowsWildcardPolicyNames());
+ assertFalse(p.allowsWildcardPolicyNames());
+
+ // Policy with wildcard
+ ArrayList observedErrors = new ArrayList<>();
+ Policy.PolicyErrorConsumer consumer = (severity, message, directiveIndex, valueIndex) -> {
+ observedErrors.add(e(severity, message, directiveIndex, valueIndex));
+ };
+ p = Policy.parseSerializedCSP("trusted-types *", consumer);
+ assertTrue(p.trustedTypes().isPresent());
+ tt = p.trustedTypes().get();
+ assertTrue(tt.allowsWildcardPolicyNames());
+ assertTrue(p.allowsWildcardPolicyNames());
+
+ // Policy without trusted-types directive
+ p = Policy.parseSerializedCSP("default-src 'self'", ThrowIfPolicyError);
+ assertFalse(p.trustedTypes().isPresent());
+ assertFalse(p.allowsWildcardPolicyNames());
+ }
+
// Helper methods
private static void roundTrips(String input, PolicyError... errors) {