Skip to content
Merged
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
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,24 @@ OpenApiServer.builder()
.build();
```

#### Built-in: browser security headers

`Handlers.securityHeadersDecorator()` adds two browser-hardening headers to every response —
`X-Content-Type-Options: nosniff` and `Cross-Origin-Resource-Policy: same-origin`. Handler-supplied
values for either header are preserved, so individual responses can opt out by setting the header
explicitly.

``` java
OpenApiServer.builder()
.spec(spec)
.handlers(handlers)
.responseDecorator(Handlers.securityHeadersDecorator())
.build();
```

Decorators run on the dispatch path only — error responses produced by `ExceptionFilter` (e.g.
the default 500) bypass them.

### Request interceptors

`Builder.interceptor(...)` registers a `RequestInterceptor` that wraps every handler invocation.
Expand Down
26 changes: 26 additions & 0 deletions src/main/java/com/retailsvc/http/Handlers.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,32 @@ public final class Handlers {

private Handlers() {}

/**
* Response decorator that adds two browser-hardening headers to every response:
*
* <ul>
* <li>{@code X-Content-Type-Options: nosniff} — prevents MIME sniffing.
* <li>{@code Cross-Origin-Resource-Policy: same-origin} — blocks cross-origin reads of the
* response body, mitigating Spectre-class side-channel attacks.
* </ul>
*
* <p>Existing headers with the same names are preserved, so a handler that sets either header
* keeps its value. Wire it in with {@code
* OpenApiServer.builder().responseDecorator(Handlers.securityHeadersDecorator())}.
*/
public static ResponseDecorator securityHeadersDecorator() {
return (request, response) -> {
Response decorated = response;
if (!response.headers().containsKey("X-Content-Type-Options")) {
decorated = decorated.withHeader("X-Content-Type-Options", "nosniff");
}
if (!response.headers().containsKey("Cross-Origin-Resource-Policy")) {
decorated = decorated.withHeader("Cross-Origin-Resource-Policy", "same-origin");
}
return decorated;
};
}

public static ExceptionHandler defaultExceptionHandler() {
return t ->
switch (t) {
Expand Down
42 changes: 42 additions & 0 deletions src/test/java/com/retailsvc/http/SecurityHeadersDecoratorTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.retailsvc.http;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;

class SecurityHeadersDecoratorTest {

private final ResponseDecorator decorator = Handlers.securityHeadersDecorator();

@Test
void addsBothHeadersWhenMissing() {
Response decorated = decorator.decorate(null, Response.status(200));

assertThat(decorated.headers())
.containsEntry("X-Content-Type-Options", "nosniff")
.containsEntry("Cross-Origin-Resource-Policy", "same-origin");
}

@Test
void preservesHandlerSuppliedNosniffValue() {
Response handlerResponse = Response.status(200).withHeader("X-Content-Type-Options", "custom");

Response decorated = decorator.decorate(null, handlerResponse);

assertThat(decorated.headers())
.containsEntry("X-Content-Type-Options", "custom")
.containsEntry("Cross-Origin-Resource-Policy", "same-origin");
}

@Test
void preservesHandlerSuppliedCorpValue() {
Response handlerResponse =
Response.status(200).withHeader("Cross-Origin-Resource-Policy", "cross-origin");

Response decorated = decorator.decorate(null, handlerResponse);

assertThat(decorated.headers())
.containsEntry("X-Content-Type-Options", "nosniff")
.containsEntry("Cross-Origin-Resource-Policy", "cross-origin");
}
}
2 changes: 2 additions & 0 deletions src/test/java/com/retailsvc/http/start/ServerLauncher.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.retailsvc.http.start;

import com.retailsvc.http.Handlers;
import com.retailsvc.http.OpenApiServer;
import com.retailsvc.http.RequestHandler;
import com.retailsvc.http.Response;
Expand Down Expand Up @@ -45,6 +46,7 @@ public ServerLauncher() throws IOException {
OpenApiServer.builder()
.spec(spec)
.handlers(handlers)
.responseDecorator(Handlers.securityHeadersDecorator())
.securityValidator("apiKeyAuth", (req, cred) -> Optional.empty())
.securityValidator("bearerAuth", (req, cred) -> Optional.empty())
.securityValidator("basicAuth", (req, cred) -> Optional.empty())
Expand Down
Loading