Skip to content

Commit 46c2153

Browse files
committed
Add allowsWildcardPolicyNames check for trusted-types
Signed-off-by: kingthorin <kingthorin@users.noreply.github.com>
1 parent 23c038b commit 46c2153

3 files changed

Lines changed: 188 additions & 5 deletions

File tree

src/main/java/org/htmlunit/csp/Policy.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,16 @@ public Optional<TrustedTypesDirective> trustedTypes() {
467467
return Optional.ofNullable(trustedTypes_);
468468
}
469469

470+
/**
471+
* Indicates if wildcard policy names are permitted in the trusted-types directive.
472+
* When true, any policy name is allowed, which may reduce security.
473+
*
474+
* @return true if wildcard policy names (*) are permitted, false if not present or not permitted
475+
*/
476+
public boolean allowsWildcardPolicyNames() {
477+
return trustedTypes_ != null && trustedTypes_.allowsWildcardPolicyNames();
478+
}
479+
470480
public Optional<RequireTrustedTypesForDirective> requireTrustedTypesFor() {
471481
return Optional.ofNullable(requireTrustedTypesFor_);
472482
}

src/main/java/org/htmlunit/csp/directive/TrustedTypesDirective.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ public TrustedTypesDirective(final List<String> values, final DirectiveErrorCons
6666
case "*":
6767
if (!star_) {
6868
star_ = true;
69+
errors.add(Policy.Severity.Warning,
70+
"Wildcard policy names (*) permit any policy name, which may reduce security", index);
6971
}
7072
else {
7173
errors.add(Policy.Severity.Warning, "Duplicate wildcard *", index);
@@ -91,11 +93,35 @@ else if (TT_POLICY_NAME_PATTERN.matcher(token).matches()) {
9193
++index;
9294
}
9395

96+
// Empty directive validation - if no values were provided, warn
97+
if (values.isEmpty()) {
98+
errors.add(Policy.Severity.Warning,
99+
"Empty trusted-types directive allows all policy names (use '*' or 'none' to be explicit)", -1);
100+
}
101+
94102
// 'none' must not be combined with other values
95103
if (none_ && (star_ || allowDuplicates_ || !policyNames_.isEmpty())) {
96104
errors.add(Policy.Severity.Error,
97105
"'none' must not be combined with any other trusted-types expression", -1);
98106
}
107+
108+
// Wildcard makes specific policy names redundant
109+
if (star_ && !policyNames_.isEmpty()) {
110+
errors.add(Policy.Severity.Warning,
111+
"Wildcard (*) permits any policy name, making specific policy names redundant", -1);
112+
}
113+
114+
// 'allow-duplicates' is redundant with wildcard (wildcard already allows everything)
115+
if (star_ && allowDuplicates_) {
116+
errors.add(Policy.Severity.Warning,
117+
"'allow-duplicates' is redundant when wildcard (*) is present", -1);
118+
}
119+
120+
// 'allow-duplicates' without policy names or wildcard has no effect
121+
if (allowDuplicates_ && !star_ && policyNames_.isEmpty()) {
122+
errors.add(Policy.Severity.Warning,
123+
"'allow-duplicates' has no effect without policy names or wildcard", -1);
124+
}
99125
}
100126

101127
public boolean none() {
@@ -136,6 +162,16 @@ public boolean star() {
136162
return star_;
137163
}
138164

165+
/**
166+
* Indicates if wildcard policy names are permitted.
167+
* When true, any policy name is allowed, which may reduce security.
168+
*
169+
* @return true if wildcard policy names (*) are permitted, false otherwise
170+
*/
171+
public boolean allowsWildcardPolicyNames() {
172+
return star_;
173+
}
174+
139175
public void setStar_(final boolean star) {
140176
if (star_ == star) {
141177
return;

src/test/java/org/htmlunit/csp/TrustedTypesTest.java

Lines changed: 142 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,19 @@ public void testTrustedTypesBasic() {
5151
assertEquals(3, tt.getPolicyNames_().size());
5252

5353
// Wildcard
54-
p = Policy.parseSerializedCSP("trusted-types *", ThrowIfPolicyError);
54+
ArrayList<PolicyError> observedErrors = new ArrayList<>();
55+
Policy.PolicyErrorConsumer consumer = (severity, message, directiveIndex, valueIndex) -> {
56+
observedErrors.add(e(severity, message, directiveIndex, valueIndex));
57+
};
58+
p = Policy.parseSerializedCSP("trusted-types *", consumer);
5559
tt = p.trustedTypes().get();
5660
assertTrue(tt.star());
61+
assertTrue(tt.allowsWildcardPolicyNames());
62+
assertTrue(p.allowsWildcardPolicyNames());
5763
assertEquals(0, tt.getPolicyNames_().size());
64+
assertEquals(1, observedErrors.size());
65+
assertEquals(Policy.Severity.Warning, observedErrors.get(0).severity_());
66+
assertTrue(observedErrors.get(0).message_().contains("Wildcard policy names"));
5867

5968
// Allow duplicates
6069
p = Policy.parseSerializedCSP("trusted-types myPolicy 'allow-duplicates'", ThrowIfPolicyError);
@@ -63,10 +72,18 @@ public void testTrustedTypesBasic() {
6372
assertEquals(1, tt.getPolicyNames_().size());
6473

6574
// Wildcard with allow-duplicates
66-
p = Policy.parseSerializedCSP("trusted-types * 'allow-duplicates'", ThrowIfPolicyError);
75+
observedErrors.clear();
76+
p = Policy.parseSerializedCSP("trusted-types * 'allow-duplicates'", consumer);
6777
tt = p.trustedTypes().get();
6878
assertTrue(tt.star());
79+
assertTrue(tt.allowsWildcardPolicyNames());
80+
assertTrue(p.allowsWildcardPolicyNames());
6981
assertTrue(tt.allowDuplicates());
82+
assertEquals(2, observedErrors.size());
83+
assertEquals(Policy.Severity.Warning, observedErrors.get(0).severity_());
84+
assertTrue(observedErrors.get(0).message_().contains("Wildcard policy names"));
85+
assertEquals(Policy.Severity.Warning, observedErrors.get(1).severity_());
86+
assertTrue(observedErrors.get(1).message_().contains("redundant when wildcard"));
7087

7188
// None keyword
7289
p = Policy.parseSerializedCSP("trusted-types 'none'", ThrowIfPolicyError);
@@ -102,23 +119,35 @@ public void testTrustedTypesPolicyNameCharacters() {
102119
public void testTrustedTypesRoundTrips() {
103120
roundTrips("trusted-types myPolicy");
104121
roundTrips("trusted-types one two three");
105-
roundTrips("trusted-types *");
122+
roundTrips("trusted-types *",
123+
e(Policy.Severity.Warning, "Wildcard policy names (*) permit any policy name, which may reduce security", 0, 0));
106124
roundTrips("trusted-types 'none'");
107125
roundTrips("trusted-types myPolicy 'allow-duplicates'");
108-
roundTrips("trusted-types * 'allow-duplicates'");
126+
roundTrips("trusted-types * 'allow-duplicates'",
127+
e(Policy.Severity.Warning, "Wildcard policy names (*) permit any policy name, which may reduce security", 0, 0),
128+
e(Policy.Severity.Warning, "'allow-duplicates' is redundant when wildcard (*) is present", 0, -1));
109129
}
110130

111131
@Test
112132
public void testTrustedTypesCaseInsensitiveKeywords() {
113133
// Keywords are case-insensitive per ABNF
114134
inTurkey(() -> {
115135
Policy p;
136+
ArrayList<PolicyError> observedErrors = new ArrayList<>();
137+
Policy.PolicyErrorConsumer consumer = (severity, message, directiveIndex, valueIndex) -> {
138+
observedErrors.add(e(severity, message, directiveIndex, valueIndex));
139+
};
116140

117141
p = Policy.parseSerializedCSP("trusted-types 'NONE'", ThrowIfPolicyError);
118142
assertTrue(p.trustedTypes().get().none());
119143

120-
p = Policy.parseSerializedCSP("trusted-types 'ALLOW-DUPLICATES'", ThrowIfPolicyError);
144+
// 'allow-duplicates' alone now generates a warning, so use consumer instead of ThrowIfPolicyError
145+
observedErrors.clear();
146+
p = Policy.parseSerializedCSP("trusted-types 'ALLOW-DUPLICATES'", consumer);
121147
assertTrue(p.trustedTypes().get().allowDuplicates());
148+
assertEquals(1, observedErrors.size());
149+
assertEquals(Policy.Severity.Warning, observedErrors.get(0).severity_());
150+
assertTrue(observedErrors.get(0).message_().contains("has no effect without policy names"));
122151

123152
p = Policy.parseSerializedCSP("TRUSTED-TYPES myPolicy", ThrowIfPolicyError);
124153
assertTrue(p.trustedTypes().isPresent());
@@ -175,14 +204,93 @@ public void testTrustedTypesErrors() {
175204
// Duplicate wildcard
176205
roundTrips(
177206
"trusted-types * *",
207+
e(Policy.Severity.Warning, "Wildcard policy names (*) permit any policy name, which may reduce security", 0, 0),
178208
e(Policy.Severity.Warning, "Duplicate wildcard *", 0, 1)
179209
);
180210

211+
// Policy name with wildcard (wildcard makes policy names redundant)
212+
roundTrips(
213+
"trusted-types myPolicy *",
214+
e(Policy.Severity.Warning, "Wildcard policy names (*) permit any policy name, which may reduce security", 0, 1),
215+
e(Policy.Severity.Warning, "Wildcard (*) permits any policy name, making specific policy names redundant", 0, -1)
216+
);
217+
218+
// Multiple policy names with wildcard
219+
roundTrips(
220+
"trusted-types one two *",
221+
e(Policy.Severity.Warning, "Wildcard policy names (*) permit any policy name, which may reduce security", 0, 2),
222+
e(Policy.Severity.Warning, "Wildcard (*) permits any policy name, making specific policy names redundant", 0, -1)
223+
);
224+
181225
// Duplicate directive
182226
roundTrips(
183227
"trusted-types one; trusted-types two",
184228
e(Policy.Severity.Warning, "Duplicate directive trusted-types", 1, -1)
185229
);
230+
231+
// Empty directive
232+
roundTrips(
233+
"trusted-types",
234+
e(Policy.Severity.Warning, "Empty trusted-types directive allows all policy names (use '*' or 'none' to be explicit)", 0, -1)
235+
);
236+
237+
// 'allow-duplicates' alone (no policy names or wildcard)
238+
roundTrips(
239+
"trusted-types 'allow-duplicates'",
240+
e(Policy.Severity.Warning, "'allow-duplicates' has no effect without policy names or wildcard", 0, -1)
241+
);
242+
243+
// Wildcard with allow-duplicates (redundant)
244+
roundTrips(
245+
"trusted-types * 'allow-duplicates'",
246+
e(Policy.Severity.Warning, "Wildcard policy names (*) permit any policy name, which may reduce security", 0, 0),
247+
e(Policy.Severity.Warning, "'allow-duplicates' is redundant when wildcard (*) is present", 0, -1)
248+
);
249+
250+
// Policy names with wildcard and allow-duplicates (multiple redundancies)
251+
roundTrips(
252+
"trusted-types myPolicy * 'allow-duplicates'",
253+
e(Policy.Severity.Warning, "Wildcard policy names (*) permit any policy name, which may reduce security", 0, 1),
254+
e(Policy.Severity.Warning, "Wildcard (*) permits any policy name, making specific policy names redundant", 0, -1),
255+
e(Policy.Severity.Warning, "'allow-duplicates' is redundant when wildcard (*) is present", 0, -1)
256+
);
257+
258+
// Order independence: wildcard before policy name
259+
roundTrips(
260+
"trusted-types * myPolicy",
261+
e(Policy.Severity.Warning, "Wildcard policy names (*) permit any policy name, which may reduce security", 0, 0),
262+
e(Policy.Severity.Warning, "Wildcard (*) permits any policy name, making specific policy names redundant", 0, -1)
263+
);
264+
}
265+
266+
@Test
267+
public void testTrustedTypesEdgeCases() {
268+
Policy p;
269+
TrustedTypesDirective tt;
270+
271+
// Single character policy name
272+
p = Policy.parseSerializedCSP("trusted-types a", ThrowIfPolicyError);
273+
tt = p.trustedTypes().get();
274+
assertEquals(1, tt.getPolicyNames_().size());
275+
assertEquals("a", tt.getPolicyNames_().get(0));
276+
277+
// Policy name with all allowed special characters
278+
p = Policy.parseSerializedCSP("trusted-types A-Za-z0-9-#=_/@.%", ThrowIfPolicyError);
279+
tt = p.trustedTypes().get();
280+
assertEquals(1, tt.getPolicyNames_().size());
281+
assertTrue(tt.getPolicyNames_().contains("A-Za-z0-9-#=_/@.%"));
282+
283+
// Policy name starting with special character
284+
p = Policy.parseSerializedCSP("trusted-types -policy", ThrowIfPolicyError);
285+
tt = p.trustedTypes().get();
286+
assertEquals(1, tt.getPolicyNames_().size());
287+
assertEquals("-policy", tt.getPolicyNames_().get(0));
288+
289+
// Policy name ending with special character
290+
p = Policy.parseSerializedCSP("trusted-types policy-", ThrowIfPolicyError);
291+
tt = p.trustedTypes().get();
292+
assertEquals(1, tt.getPolicyNames_().size());
293+
assertEquals("policy-", tt.getPolicyNames_().get(0));
186294
}
187295

188296
// require-trusted-types-for directive tests
@@ -286,6 +394,35 @@ public void testRequireTrustedTypesForManipulation() {
286394
assertTrue(rttf.script());
287395
}
288396

397+
@Test
398+
public void testAllowsWildcardPolicyNames() {
399+
Policy p;
400+
TrustedTypesDirective tt;
401+
402+
// Policy without wildcard
403+
p = Policy.parseSerializedCSP("trusted-types myPolicy", ThrowIfPolicyError);
404+
assertTrue(p.trustedTypes().isPresent());
405+
tt = p.trustedTypes().get();
406+
assertFalse(tt.allowsWildcardPolicyNames());
407+
assertFalse(p.allowsWildcardPolicyNames());
408+
409+
// Policy with wildcard
410+
ArrayList<PolicyError> observedErrors = new ArrayList<>();
411+
Policy.PolicyErrorConsumer consumer = (severity, message, directiveIndex, valueIndex) -> {
412+
observedErrors.add(e(severity, message, directiveIndex, valueIndex));
413+
};
414+
p = Policy.parseSerializedCSP("trusted-types *", consumer);
415+
assertTrue(p.trustedTypes().isPresent());
416+
tt = p.trustedTypes().get();
417+
assertTrue(tt.allowsWildcardPolicyNames());
418+
assertTrue(p.allowsWildcardPolicyNames());
419+
420+
// Policy without trusted-types directive
421+
p = Policy.parseSerializedCSP("default-src 'self'", ThrowIfPolicyError);
422+
assertFalse(p.trustedTypes().isPresent());
423+
assertFalse(p.allowsWildcardPolicyNames());
424+
}
425+
289426
// Helper methods
290427

291428
private static void roundTrips(String input, PolicyError... errors) {

0 commit comments

Comments
 (0)