Skip to content
Draft
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
30 changes: 30 additions & 0 deletions src/main/java/org/htmlunit/csp/Policy.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@
import org.htmlunit.csp.directive.HostSourceDirective;
import org.htmlunit.csp.directive.PluginTypesDirective;
import org.htmlunit.csp.directive.ReportUriDirective;
import org.htmlunit.csp.directive.RequireTrustedTypesForDirective;
import org.htmlunit.csp.directive.SandboxDirective;
import org.htmlunit.csp.directive.SourceExpressionDirective;
import org.htmlunit.csp.directive.TrustedTypesDirective;
import org.htmlunit.csp.url.GUID;
import org.htmlunit.csp.url.URI;
import org.htmlunit.csp.url.URLWithScheme;
Expand Down Expand Up @@ -70,6 +72,8 @@ public final class Policy {
private RFC7230Token reportTo_;
private ReportUriDirective reportUri_;
private SandboxDirective sandbox_;
private TrustedTypesDirective trustedTypes_;
private RequireTrustedTypesForDirective requireTrustedTypes_;
private boolean upgradeInsecureRequests_;

private final EnumMap<FetchDirectiveKind, SourceExpressionDirective> fetchDirectives_
Expand Down Expand Up @@ -318,6 +322,32 @@ else if (values.size() == 1) {
newDirective = sandboxDirective;
break;

case "trusted-types":
// https://www.w3.org/TR/trusted-types/
final TrustedTypesDirective trustedTypesDirective = new TrustedTypesDirective(
values, directiveErrorConsumer);
if (trustedTypes_ == null) {
trustedTypes_ = trustedTypesDirective;
}
else {
wasDupe = true;
}
newDirective = trustedTypesDirective;
break;

case "require-trusted-types-for":
// https://www.w3.org/TR/trusted-types/#require-trusted-types-for-csp-directive
final RequireTrustedTypesForDirective requireTrustedTypesDirective = new RequireTrustedTypesForDirective(
values, directiveErrorConsumer);
if (requireTrustedTypes_ == null) {
requireTrustedTypes_ = requireTrustedTypesDirective;
}
else {
wasDupe = true;
}
newDirective = requireTrustedTypesDirective;
break;

case "upgrade-insecure-requests":
// https://www.w3.org/TR/upgrade-insecure-requests/#delivery
if (upgradeInsecureRequests_) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright (c) 2023-2026 Ronald Brill.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.htmlunit.csp.directive;

import org.htmlunit.csp.Directive;
import org.htmlunit.csp.Policy;
import java.util.List;

/**
* Directive implementation for `require-trusted-types-for`.
*/
public class RequireTrustedTypesForDirective extends Directive {

public RequireTrustedTypesForDirective(final List<String> values, final DirectiveErrorConsumer errors) {
super(values);
if (!values.isEmpty()) {
for (int i = 0; i < values.size(); i++) {
String value = values.get(i);
if (!"script".equalsIgnoreCase(value)) {
errors.add(Policy.Severity.Error, "`require-trusted-types-for` only accepts 'script' as a value.", i);
}
}
}
}
}
109 changes: 109 additions & 0 deletions src/main/java/org/htmlunit/csp/directive/TrustedTypesDirective.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Copyright (c) 2023-2026 Ronald Brill.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.htmlunit.csp.directive;

import org.htmlunit.csp.Directive;
import org.htmlunit.csp.Policy;

import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;

/**
* Directive implementation for `trusted-types`.
*/
public class TrustedTypesDirective extends Directive {

private boolean allowDuplicates_;
private boolean allowAnyPolicyName_;
private Set<String> allowedPolicyNames_ = new HashSet<>();

public TrustedTypesDirective(final List<String> values, final DirectiveErrorConsumer errors) {
super(values);
boolean hasNone = values.stream().anyMatch(x -> x.toLowerCase(Locale.ROOT).equals("'none'"));

if (hasNone && values.size() > 1) {
errors.add(Policy.Severity.Error,
"Specifying trusted-types 'none' along with other values is invalid", -1);
}

for (int i = 0; i < values.size(); i++) {
String value = values.get(i);
final String lowercaseValue = value.toLowerCase(Locale.ROOT);

if ("*".equals(value)) {
allowAnyPolicyName_ = true;
continue;
} else if (value.isBlank()) {
errors.add(Policy.Severity.Error, "Empty or whitespace-only policy name is not allowed.", i);
continue;
}

// Check for quoted keywords
if (value.startsWith("'") && value.endsWith("'") && value.length() > 2) { // Minimum "'x'"
String inner = lowercaseValue.substring(1, value.length() - 1);
if (inner.equals("allow-duplicates")) {
allowDuplicates_ = true;
} else if (inner.equals("none")) {
// 'none' is ignored here, nNo action needed
} else {
errors.add(Policy.Severity.Error, "Unknown keyword in trusted-types: " + value, i);
}
continue;
}

// Validate and add policy name (unquoted)
if (isValidPolicyName(value)) {
if (!allowedPolicyNames_.add(value)) { // False when attempting to add duplicate
// The spec treats the policy name as a JavaScript DOMString identifier, and comparisons are done as exact string matches.
// There is no case normalization step.
// https://www.w3.org/TR/trusted-types/#should-trusted-type-policy-creation-be-blocked-by-content-security-policy
// https://www.w3.org/TR/trusted-types/#create-a-trusted-type-policy
// Case-sensitive storage
errors.add(Policy.Severity.Warning, "Second attempt to add trusted-types policy: " + value, i);
};
} else {
errors.add(Policy.Severity.Error, "Invalid policy name in trusted-types: " + value
+ " (must be alphanumeric or -#=_/@.%)", i);
}
}
}

private static boolean isValidPolicyName(String name) {
if (name.isEmpty()) {
return false;
}
for (char c : name.toCharArray()) {
if (!Character.isLetterOrDigit(c) && "-#=_/@.%".indexOf(c) == -1) {
return false;
}
}
return true;
}

public boolean isAllowDuplicates() {
return allowDuplicates_;
}

public boolean isAllowAnyPolicyName() {
return allowAnyPolicyName_;
}

public Set<String> getAllowedPolicyNames() {
return Collections.unmodifiableSet(allowedPolicyNames_);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright (c) 2023-2026 Ronald Brill.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.htmlunit.csp.directive;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.ArrayList;
import java.util.List;

class RequireTrustedTypesForDirectiveTest {

@Test
void testValidScriptValue() {
// Given
List<String> errors = new ArrayList<>();
// When
// Valid value: 'script'
RequireTrustedTypesForDirective directive = new RequireTrustedTypesForDirective(
List.of("script"), (severity, message, valueIndex) -> errors.add(message));
// Then
assertTrue(directive.getValues().contains("script"));
assertTrue(errors.isEmpty()); // No errors should be reported
}

@Test
void testInvalidValue() {
List<String> errors = new ArrayList<>();
// Then
// Invalid value: 'invalid'
RequireTrustedTypesForDirective directive = new RequireTrustedTypesForDirective(
List.of("invalid"), (severity, message, valueIndex) -> errors.add(message));
// Then
assertTrue(directive.getValues().contains("invalid"));
assertEquals(1, errors.size()); // One error should be reported
assertEquals("`require-trusted-types-for` only accepts 'script' as a value.",
errors.get(0));
}

@Test
void testInvalidValueAmonstMany() {
// Given
List<String> errors = new ArrayList<>();
// When
// Invalid value: 'invalid'
RequireTrustedTypesForDirective directive = new RequireTrustedTypesForDirective(
List.of("script", "invalid"), (severity, message, valueIndex) -> errors.add(message));
// Then
assertTrue(directive.getValues().contains("script"));
assertTrue(directive.getValues().contains("invalid"));
assertEquals(1, errors.size()); // One error should be reported
assertEquals("`require-trusted-types-for` only accepts 'script' as a value.",
errors.get(0));
}
}
Loading