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) {