Add security headers functionality with opt-in HSTS, CSP, and other browser-hardening features
This commit is contained in:
@@ -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"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user