Add security headers functionality with opt-in HSTS, CSP, and other browser-hardening features
CI - Test, Publish and Release / run-tests (push) Successful in 19s
CI - Test, Publish and Release / create-release (push) Successful in 19s
CI - Test, Publish and Release / check-and-publish (push) Successful in 13s

This commit is contained in:
2026-06-15 07:17:35 +02:00
parent bcf5572aeb
commit a0790400e2
8 changed files with 504 additions and 5 deletions
@@ -116,4 +116,18 @@ class AuthenticatorTest {
Authenticator auth = Authenticator.apiKey("X-API-Key", k -> Principal.of("never"));
assertNull(auth.authenticate(request("X-API-Key", "")));
}
@Test
void constantTimeEqualsMatchesIdenticalValues() {
assertTrue(Authenticator.constantTimeEquals("s3cr3t", "s3cr3t"));
}
@Test
void constantTimeEqualsRejectsDifferentValuesAndNulls() {
assertFalse(Authenticator.constantTimeEquals("s3cr3t", "s3cr3T"));
assertFalse(Authenticator.constantTimeEquals("short", "longer-value"));
assertFalse(Authenticator.constantTimeEquals(null, "x"));
assertFalse(Authenticator.constantTimeEquals("x", null));
assertFalse(Authenticator.constantTimeEquals(null, null));
}
}
@@ -0,0 +1,105 @@
package dev.coph.nextusweb.server.security;
import dev.coph.nextusweb.server.router.Response;
import org.junit.jupiter.api.Test;
import java.time.Duration;
import static org.junit.jupiter.api.Assertions.*;
class SecurityHeadersTest {
@Test
void defaultsApplyConservativeHeaders() {
Response res = new Response();
SecurityHeaders.defaults().apply(res, false);
assertEquals("nosniff", res.headers().get("X-Content-Type-Options"));
assertEquals("DENY", res.headers().get("X-Frame-Options"));
assertEquals("no-referrer", res.headers().get("Referrer-Policy"));
assertNull(res.headers().get("Content-Security-Policy"));
}
@Test
void hstsIsOmittedOnInsecureConnections() {
Response res = new Response();
SecurityHeaders.defaults().apply(res, false);
assertNull(res.headers().get("Strict-Transport-Security"));
}
@Test
void hstsIsEmittedOnSecureConnections() {
Response res = new Response();
SecurityHeaders.defaults().apply(res, true);
assertEquals("max-age=31536000", res.headers().get("Strict-Transport-Security"));
}
@Test
void hstsRendersIncludeSubDomainsAndPreload() {
SecurityHeaders sh = SecurityHeaders.builder()
.hsts(Duration.ofDays(365), true, true)
.build();
Response res = new Response();
sh.apply(res, true);
assertEquals("max-age=31536000; includeSubDomains; preload",
res.headers().get("Strict-Transport-Security"));
}
@Test
void noHstsDisablesTheHeaderEvenWhenSecure() {
SecurityHeaders sh = SecurityHeaders.builder().noHsts().build();
Response res = new Response();
sh.apply(res, true);
assertNull(res.headers().get("Strict-Transport-Security"));
}
@Test
void existingHandlerHeadersAreNotOverwritten() {
Response res = new Response();
res.header("X-Frame-Options", "SAMEORIGIN");
SecurityHeaders.defaults().apply(res, true);
assertEquals("SAMEORIGIN", res.headers().get("X-Frame-Options"));
// Other headers the handler did not set are still added.
assertEquals("nosniff", res.headers().get("X-Content-Type-Options"));
}
@Test
void existingHstsHeaderIsNotOverwritten() {
Response res = new Response();
res.header("Strict-Transport-Security", "max-age=60");
SecurityHeaders.defaults().apply(res, true);
assertEquals("max-age=60", res.headers().get("Strict-Transport-Security"));
}
@Test
void disabledHeadersAreOmitted() {
SecurityHeaders sh = SecurityHeaders.builder()
.contentTypeOptions(false)
.frameOptions(null)
.referrerPolicy(" ")
.noHsts()
.build();
Response res = new Response();
sh.apply(res, true);
assertNull(res.headers().get("X-Content-Type-Options"));
assertNull(res.headers().get("X-Frame-Options"));
assertNull(res.headers().get("Referrer-Policy"));
assertNull(res.headers().get("Strict-Transport-Security"));
}
@Test
void contentSecurityPolicyAndCustomHeaderAreApplied() {
SecurityHeaders sh = SecurityHeaders.builder()
.contentSecurityPolicy("default-src 'self'")
.header("Permissions-Policy", "geolocation=()")
.build();
Response res = new Response();
sh.apply(res, false);
assertEquals("default-src 'self'", res.headers().get("Content-Security-Policy"));
assertEquals("geolocation=()", res.headers().get("Permissions-Policy"));
}
}