Skip to content

Commit dadd983

Browse files
committed
feat: Add securityHeadersDecorator for browser hardening
Provide an opt-in ResponseDecorator that sets two browser-hardening headers on every response routed through the OpenAPI dispatch chain: - X-Content-Type-Options: nosniff - Cross-Origin-Resource-Policy: same-origin Both are skipped when the handler has already set the header, so per-response overrides keep working. Wire in with OpenApiServer.builder().responseDecorator(Handlers.securityHeadersDecorator()). ServerLauncher now applies the decorator so the local demo and the ZAP scan exercise it. Note: ResponseDecorator runs in the dispatch chain, not in ExceptionFilter, so 500 responses produced by the default exception path remain unaffected. That's an intentional scope limit for this change.
1 parent d61095a commit dadd983

3 files changed

Lines changed: 70 additions & 0 deletions

File tree

src/main/java/com/retailsvc/http/Handlers.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,32 @@ public final class Handlers {
2828

2929
private Handlers() {}
3030

31+
/**
32+
* Response decorator that adds two browser-hardening headers to every response:
33+
*
34+
* <ul>
35+
* <li>{@code X-Content-Type-Options: nosniff} — prevents MIME sniffing.
36+
* <li>{@code Cross-Origin-Resource-Policy: same-origin} — blocks cross-origin reads of the
37+
* response body, mitigating Spectre-class side-channel attacks.
38+
* </ul>
39+
*
40+
* <p>Existing headers with the same names are preserved, so a handler that sets either header
41+
* keeps its value. Wire it in with {@code
42+
* OpenApiServer.builder().responseDecorator(Handlers.securityHeadersDecorator())}.
43+
*/
44+
public static ResponseDecorator securityHeadersDecorator() {
45+
return (request, response) -> {
46+
Response decorated = response;
47+
if (!response.headers().containsKey("X-Content-Type-Options")) {
48+
decorated = decorated.withHeader("X-Content-Type-Options", "nosniff");
49+
}
50+
if (!response.headers().containsKey("Cross-Origin-Resource-Policy")) {
51+
decorated = decorated.withHeader("Cross-Origin-Resource-Policy", "same-origin");
52+
}
53+
return decorated;
54+
};
55+
}
56+
3157
public static ExceptionHandler defaultExceptionHandler() {
3258
return t ->
3359
switch (t) {
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.retailsvc.http;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import org.junit.jupiter.api.Test;
6+
7+
class SecurityHeadersDecoratorTest {
8+
9+
private final ResponseDecorator decorator = Handlers.securityHeadersDecorator();
10+
11+
@Test
12+
void addsBothHeadersWhenMissing() {
13+
Response decorated = decorator.decorate(null, Response.status(200));
14+
15+
assertThat(decorated.headers())
16+
.containsEntry("X-Content-Type-Options", "nosniff")
17+
.containsEntry("Cross-Origin-Resource-Policy", "same-origin");
18+
}
19+
20+
@Test
21+
void preservesHandlerSuppliedNosniffValue() {
22+
Response handlerResponse = Response.status(200).withHeader("X-Content-Type-Options", "custom");
23+
24+
Response decorated = decorator.decorate(null, handlerResponse);
25+
26+
assertThat(decorated.headers())
27+
.containsEntry("X-Content-Type-Options", "custom")
28+
.containsEntry("Cross-Origin-Resource-Policy", "same-origin");
29+
}
30+
31+
@Test
32+
void preservesHandlerSuppliedCorpValue() {
33+
Response handlerResponse =
34+
Response.status(200).withHeader("Cross-Origin-Resource-Policy", "cross-origin");
35+
36+
Response decorated = decorator.decorate(null, handlerResponse);
37+
38+
assertThat(decorated.headers())
39+
.containsEntry("X-Content-Type-Options", "nosniff")
40+
.containsEntry("Cross-Origin-Resource-Policy", "cross-origin");
41+
}
42+
}

src/test/java/com/retailsvc/http/start/ServerLauncher.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.retailsvc.http.start;
22

3+
import com.retailsvc.http.Handlers;
34
import com.retailsvc.http.OpenApiServer;
45
import com.retailsvc.http.RequestHandler;
56
import com.retailsvc.http.Response;
@@ -45,6 +46,7 @@ public ServerLauncher() throws IOException {
4546
OpenApiServer.builder()
4647
.spec(spec)
4748
.handlers(handlers)
49+
.responseDecorator(Handlers.securityHeadersDecorator())
4850
.securityValidator("apiKeyAuth", (req, cred) -> Optional.empty())
4951
.securityValidator("bearerAuth", (req, cred) -> Optional.empty())
5052
.securityValidator("basicAuth", (req, cred) -> Optional.empty())

0 commit comments

Comments
 (0)