Skip to content

Commit 23c038b

Browse files
committed
add support for 'trusted-types' and 'require-trusted-types-for' CSP directives
this is based on shapesecurity/salvation#273
1 parent 4082f6b commit 23c038b

5 files changed

Lines changed: 592 additions & 2 deletions

File tree

pom.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,9 @@
337337
<contributor>
338338
<name>Rick Mitchell</name>
339339
</contributor>
340+
<contributor>
341+
<name>Michael Smith</name>
342+
</contributor>
340343
</contributors>
341344

342345
<dependencies>

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

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@
3333
import org.htmlunit.csp.directive.HostSourceDirective;
3434
import org.htmlunit.csp.directive.PluginTypesDirective;
3535
import org.htmlunit.csp.directive.ReportUriDirective;
36+
import org.htmlunit.csp.directive.RequireTrustedTypesForDirective;
3637
import org.htmlunit.csp.directive.SandboxDirective;
3738
import org.htmlunit.csp.directive.SourceExpressionDirective;
39+
import org.htmlunit.csp.directive.TrustedTypesDirective;
3840
import org.htmlunit.csp.url.GUID;
3941
import org.htmlunit.csp.url.URI;
4042
import org.htmlunit.csp.url.URLWithScheme;
@@ -44,7 +46,7 @@
4446
import org.htmlunit.csp.value.RFC7230Token;
4547
import org.htmlunit.csp.value.Scheme;
4648

47-
public final class Policy {
49+
public class Policy {
4850
// Things we don't preserve:
4951
// - Whitespace
5052
// - Empty directives or policies (as in `; ;` or `, ,`)
@@ -60,12 +62,16 @@ public final class Policy {
6062

6163
private final List<NamedDirective> directives_ = new ArrayList<>();
6264

63-
private SourceExpressionDirective baseUri_;
6465
private boolean blockAllMixedContent_;
66+
67+
private SourceExpressionDirective baseUri_;
6568
private SourceExpressionDirective formAction_;
6669
private FrameAncestorsDirective frameAncestors_;
6770
private SourceExpressionDirective navigateTo_;
6871
private PluginTypesDirective pluginTypes_;
72+
private TrustedTypesDirective trustedTypes_;
73+
private RequireTrustedTypesForDirective requireTrustedTypesFor_;
74+
6975
private FetchDirectiveKind prefetchSrc_;
7076
private RFC7230Token reportTo_;
7177
private ReportUriDirective reportUri_;
@@ -320,6 +326,32 @@ else if (values.size() == 1) {
320326
newDirective = sandboxDirective;
321327
break;
322328

329+
case "trusted-types":
330+
// https://w3c.github.io/trusted-types/dist/spec/#trusted-types-csp-directive
331+
final TrustedTypesDirective trustedTypesDirective =
332+
new TrustedTypesDirective(values, directiveErrorConsumer);
333+
if (this.trustedTypes_ == null) {
334+
this.trustedTypes_ = trustedTypesDirective;
335+
}
336+
else {
337+
wasDupe = true;
338+
}
339+
newDirective = trustedTypesDirective;
340+
break;
341+
342+
case "require-trusted-types-for":
343+
// https://w3c.github.io/trusted-types/dist/spec/#require-trusted-types-for-csp-directive
344+
final RequireTrustedTypesForDirective requireTrustedTypesForDirective =
345+
new RequireTrustedTypesForDirective(values, directiveErrorConsumer);
346+
if (this.requireTrustedTypesFor_ == null) {
347+
this.requireTrustedTypesFor_ = requireTrustedTypesForDirective;
348+
}
349+
else {
350+
wasDupe = true;
351+
}
352+
newDirective = requireTrustedTypesForDirective;
353+
break;
354+
323355
case "upgrade-insecure-requests":
324356
// https://www.w3.org/TR/upgrade-insecure-requests/#delivery
325357
if (upgradeInsecureRequests_) {
@@ -431,6 +463,14 @@ public Optional<SandboxDirective> sandbox() {
431463
return Optional.ofNullable(sandbox_);
432464
}
433465

466+
public Optional<TrustedTypesDirective> trustedTypes() {
467+
return Optional.ofNullable(trustedTypes_);
468+
}
469+
470+
public Optional<RequireTrustedTypesForDirective> requireTrustedTypesFor() {
471+
return Optional.ofNullable(requireTrustedTypesFor_);
472+
}
473+
434474
public boolean upgradeInsecureRequests() {
435475
return upgradeInsecureRequests_;
436476
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* Copyright (c) 2023-2026 Ronald Brill.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* https://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
package org.htmlunit.csp.directive;
16+
17+
import org.htmlunit.csp.Directive;
18+
import org.htmlunit.csp.Policy;
19+
20+
import java.util.List;
21+
import java.util.Locale;
22+
23+
/**
24+
* @author Michael Smith
25+
*/
26+
public class RequireTrustedTypesForDirective extends Directive {
27+
// https://w3c.github.io/trusted-types/dist/spec/#require-trusted-types-for-csp-directive
28+
// Currently only 'script' is defined
29+
private static final String SCRIPT = "'script'";
30+
31+
private boolean script_ = false;
32+
33+
public RequireTrustedTypesForDirective(final List<String> values, final DirectiveErrorConsumer errors) {
34+
super(values);
35+
36+
if (values.isEmpty()) {
37+
errors.add(Policy.Severity.Error, "The require-trusted-types-for directive requires a value", -1);
38+
return;
39+
}
40+
41+
int index = 0;
42+
for (final String token : values) {
43+
// ABNF strings are case-insensitive
44+
final String lowcaseToken = token.toLowerCase(Locale.ROOT);
45+
switch (lowcaseToken) {
46+
case "'script'":
47+
if (!script_) {
48+
script_ = true;
49+
}
50+
else {
51+
errors.add(Policy.Severity.Warning, "Duplicate keyword 'script'", index);
52+
}
53+
break;
54+
default:
55+
if (token.startsWith("'") && token.endsWith("'")) {
56+
errors.add(Policy.Severity.Error,
57+
"Unrecognized require-trusted-types-for keyword " + token, index);
58+
}
59+
else {
60+
errors.add(Policy.Severity.Error,
61+
"Unrecognized require-trusted-types-for value " + token
62+
+ " - keywords must be wrapped in single quotes", index);
63+
}
64+
}
65+
++index;
66+
}
67+
}
68+
69+
public boolean script() {
70+
return script_;
71+
}
72+
73+
public void setScript_(final boolean script) {
74+
if (script_ == script) {
75+
return;
76+
}
77+
if (script) {
78+
addValue(SCRIPT);
79+
}
80+
else {
81+
removeValueIgnoreCase(SCRIPT);
82+
}
83+
script_ = script;
84+
}
85+
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/*
2+
* Copyright (c) 2023-2026 Ronald Brill.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* https://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
package org.htmlunit.csp.directive;
16+
17+
import org.htmlunit.csp.Directive;
18+
import org.htmlunit.csp.Policy;
19+
20+
import java.util.ArrayList;
21+
import java.util.Collections;
22+
import java.util.List;
23+
import java.util.Locale;
24+
import java.util.regex.Pattern;
25+
26+
/**
27+
* @author Michael Smith
28+
*/
29+
public class TrustedTypesDirective extends Directive {
30+
// https://w3c.github.io/trusted-types/dist/spec/#trusted-types-csp-directive
31+
// tt-policy-name = 1*( ALPHA / DIGIT / "-" / "#" / "=" / "_" / "/" / "@" / "." / "%" )
32+
private static final Pattern TT_POLICY_NAME_PATTERN = Pattern.compile("^[A-Za-z0-9\\-#=_/@.%]+$");
33+
34+
private boolean none_ = false;
35+
private boolean allowDuplicates_ = false;
36+
private boolean star_ = false;
37+
private final List<String> policyNames_ = new ArrayList<>();
38+
39+
public TrustedTypesDirective(final List<String> values, final DirectiveErrorConsumer errors) {
40+
super(values);
41+
42+
int index = 0;
43+
for (final String token : values) {
44+
// Keywords are case-insensitive per ABNF spec (RFC 5234 §2.3).
45+
// Note: Chromium incorrectly treats 'allow-duplicates' as case-sensitive,
46+
// while WebKit correctly treats it as case-insensitive. We follow the spec.
47+
// See https://issues.chromium.org/issues/472892238
48+
final String lowcaseToken = token.toLowerCase(Locale.ROOT);
49+
switch (lowcaseToken) {
50+
case "'none'":
51+
if (!none_) {
52+
none_ = true;
53+
}
54+
else {
55+
errors.add(Policy.Severity.Warning, "Duplicate keyword 'none'", index);
56+
}
57+
break;
58+
case "'allow-duplicates'":
59+
if (!allowDuplicates_) {
60+
allowDuplicates_ = true;
61+
}
62+
else {
63+
errors.add(Policy.Severity.Warning, "Duplicate keyword 'allow-duplicates'", index);
64+
}
65+
break;
66+
case "*":
67+
if (!star_) {
68+
star_ = true;
69+
}
70+
else {
71+
errors.add(Policy.Severity.Warning, "Duplicate wildcard *", index);
72+
}
73+
break;
74+
default:
75+
if (token.startsWith("'") && token.endsWith("'")) {
76+
errors.add(Policy.Severity.Error, "Unrecognized trusted-types keyword " + token, index);
77+
}
78+
else if (TT_POLICY_NAME_PATTERN.matcher(token).matches()) {
79+
// Policy names are case-sensitive per browser behavior
80+
if (policyNames_.contains(token)) {
81+
errors.add(Policy.Severity.Warning, "Duplicate policy name " + token, index);
82+
}
83+
else {
84+
policyNames_.add(token);
85+
}
86+
}
87+
else {
88+
errors.add(Policy.Severity.Error, "Invalid trusted-types policy name " + token, index);
89+
}
90+
}
91+
++index;
92+
}
93+
94+
// 'none' must not be combined with other values
95+
if (none_ && (star_ || allowDuplicates_ || !policyNames_.isEmpty())) {
96+
errors.add(Policy.Severity.Error,
97+
"'none' must not be combined with any other trusted-types expression", -1);
98+
}
99+
}
100+
101+
public boolean none() {
102+
return none_;
103+
}
104+
105+
public void setNone(final boolean none) {
106+
if (none_ == none) {
107+
return;
108+
}
109+
if (none) {
110+
addValue("'none'");
111+
}
112+
else {
113+
removeValueIgnoreCase("'none'");
114+
}
115+
none_ = none;
116+
}
117+
118+
public boolean allowDuplicates() {
119+
return allowDuplicates_;
120+
}
121+
122+
public void setAllowDuplicates_(final boolean allowDuplicates) {
123+
if (allowDuplicates_ == allowDuplicates) {
124+
return;
125+
}
126+
if (allowDuplicates) {
127+
addValue("'allow-duplicates'");
128+
}
129+
else {
130+
removeValueIgnoreCase("'allow-duplicates'");
131+
}
132+
allowDuplicates_ = allowDuplicates;
133+
}
134+
135+
public boolean star() {
136+
return star_;
137+
}
138+
139+
public void setStar_(final boolean star) {
140+
if (star_ == star) {
141+
return;
142+
}
143+
if (star) {
144+
addValue("*");
145+
}
146+
else {
147+
removeValueIgnoreCase("*");
148+
}
149+
star_ = star;
150+
}
151+
152+
public List<String> getPolicyNames_() {
153+
return Collections.unmodifiableList(policyNames_);
154+
}
155+
}

0 commit comments

Comments
 (0)