Add security headers functionality with opt-in HSTS, CSP, and other browser-hardening features
This commit is contained in:
@@ -12,6 +12,7 @@ A lightweight, high-performance HTTP server library built on top of Netty. Nexus
|
||||
- **Annotation-based controllers** — define routes declaratively with `@Controller`, `@GET`, `@POST`, etc.
|
||||
- **Middleware chain** — attach cross-cutting logic to all routes
|
||||
- **CORS support** — configurable origins, methods, headers, credentials, and preflight caching
|
||||
- **Security headers** — opt-in `nosniff`, `X-Frame-Options`, `Referrer-Policy`, CSP and HTTPS-only HSTS, applied to every response
|
||||
- **Rate limiting** — four algorithm implementations with per-IP, per-header, per-cookie, per-principal or custom key strategies, with automatic eviction of idle state
|
||||
- **Spoofing-safe client IP** — `X-Forwarded-For` is honoured only behind configured trusted proxies
|
||||
- **WebSockets** — path-routed handlers with origin validation, optional authentication, ordered per-connection delivery, backpressure, idle timeout, frame size limits and permessage-deflate
|
||||
@@ -294,6 +295,13 @@ router.get("/api/me", (req, res) -> {
|
||||
|
||||
`validator` returns the resolved `Principal`, or `null` for missing/invalid credentials (→ `401` on a `REQUIRED` path). A thrown exception is treated as an internal error (→ generic `500`); details are logged, never sent to the client. Rate limiting runs **before** authentication, so an unauthenticated flood is shed before reaching a (potentially expensive) authenticator.
|
||||
|
||||
When a validator compares a presented secret (API key, token, password) against an expected value, use `Authenticator.constantTimeEquals(presented, expected)` instead of `String.equals` to avoid leaking how many characters matched through a timing side channel:
|
||||
|
||||
```java
|
||||
Authenticator auth = Authenticator.apiKey("X-API-Key",
|
||||
key -> Authenticator.constantTimeEquals(key, EXPECTED_KEY) ? Principal.of("svc") : null);
|
||||
```
|
||||
|
||||
WebSocket upgrades on protected paths are authenticated the same way; the resolved principal is available via `session.principal()`.
|
||||
|
||||
---
|
||||
@@ -332,6 +340,51 @@ HttpServer.builder(8080, router)
|
||||
|
||||
---
|
||||
|
||||
## Security headers
|
||||
|
||||
`withSecurityHeaders(...)` adds standard browser-hardening response headers to **every** response (handler responses, errors, CORS preflights and rejections alike). It is opt-in; without the call no security headers are sent.
|
||||
|
||||
```java
|
||||
import dev.coph.nextusweb.server.security.SecurityHeaders;
|
||||
|
||||
HttpServer.builder(443, router)
|
||||
.withTls(TlsConfig.fromPem(cert, key))
|
||||
.withSecurityHeaders(SecurityHeaders.defaults())
|
||||
.start();
|
||||
```
|
||||
|
||||
`SecurityHeaders.defaults()` emits a conservative baseline:
|
||||
|
||||
| Header | Value | Notes |
|
||||
|---|---|---|
|
||||
| `X-Content-Type-Options` | `nosniff` | Blocks MIME sniffing |
|
||||
| `X-Frame-Options` | `DENY` | Click-jacking defence |
|
||||
| `Referrer-Policy` | `no-referrer` | No referrer leakage |
|
||||
| `Strict-Transport-Security` | `max-age=31536000` | **Only sent over HTTPS** (when `withTls(...)` is set); pins HTTPS for a year |
|
||||
|
||||
Two safety rules keep it from breaking anything: **HSTS is emitted only on TLS connections** (a browser ignores it on plain HTTP), and a header a handler has **already set is never overwritten** — so per-route choices win.
|
||||
|
||||
For full control use the builder. Passing `null`/blank to a setter omits that header:
|
||||
|
||||
```java
|
||||
SecurityHeaders headers = SecurityHeaders.builder()
|
||||
.frameOptions("SAMEORIGIN") // or null to omit
|
||||
.referrerPolicy("strict-origin-when-cross-origin")
|
||||
.contentSecurityPolicy("default-src 'self'") // off by default (app-specific)
|
||||
.hsts(Duration.ofDays(365), true, false) // maxAge, includeSubDomains, preload
|
||||
.header("Permissions-Policy", "geolocation=()") // any extra header
|
||||
.build();
|
||||
|
||||
HttpServer.builder(443, router)
|
||||
.withTls(TlsConfig.fromPem(cert, key))
|
||||
.withSecurityHeaders(headers)
|
||||
.start();
|
||||
```
|
||||
|
||||
> `includeSubDomains` and `preload` are hard to roll back — enable them only once every subdomain is reliably served over HTTPS.
|
||||
|
||||
---
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
### Algorithms
|
||||
@@ -543,6 +596,7 @@ WebSocketConfig wsConfig = WebSocketConfig.builder()
|
||||
HttpServer.builder(8080, router)
|
||||
.withCorsHandler(cors)
|
||||
.withRateLimitGate(gate)
|
||||
.withSecurityHeaders(SecurityHeaders.defaults())
|
||||
.withWebSockets(wsRouter, wsConfig)
|
||||
.start();
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user