Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/main/java/org/htmlunit/csp/Policy.java
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,16 @@ public Optional<TrustedTypesDirective> 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<RequireTrustedTypesForDirective> requireTrustedTypesFor() {
return Optional.ofNullable(requireTrustedTypesFor_);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ public TrustedTypesDirective(final List<String> 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);
Expand All @@ -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() {
Expand Down Expand Up @@ -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;
Expand Down
147 changes: 142 additions & 5 deletions src/test/java/org/htmlunit/csp/TrustedTypesTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,19 @@ public void testTrustedTypesBasic() {
assertEquals(3, tt.getPolicyNames_().size());

// Wildcard
p = Policy.parseSerializedCSP("trusted-types *", ThrowIfPolicyError);
ArrayList<PolicyError> 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);
Expand All @@ -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);
Expand Down Expand Up @@ -102,23 +119,35 @@ 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
public void testTrustedTypesCaseInsensitiveKeywords() {
// Keywords are case-insensitive per ABNF
inTurkey(() -> {
Policy p;
ArrayList<PolicyError> 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());
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<PolicyError> 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) {
Expand Down