diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 00000000..e039f463 --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,4 @@ +## 2026-06-29 - Missing Global API Security Headers +**Vulnerability:** API endpoints lacking basic security headers like `X-Content-Type-Options: nosniff`. +**Learning:** Security header filters (e.g., `ViewerSecurityHeadersWebFilter.java`) are sometimes overly specific in their path matching, leaving other critical API endpoints completely unprotected. +**Prevention:** Always ensure standard security headers (`X-Content-Type-Options`, `Cache-Control`) are applied globally to all responses, while reserving specific headers (like strict CSPs) for HTML-serving surfaces. diff --git a/src/main/java/com/clearfolio/viewer/config/ViewerSecurityHeadersWebFilter.java b/src/main/java/com/clearfolio/viewer/config/ViewerSecurityHeadersWebFilter.java index 798246b1..09a3f67e 100644 --- a/src/main/java/com/clearfolio/viewer/config/ViewerSecurityHeadersWebFilter.java +++ b/src/main/java/com/clearfolio/viewer/config/ViewerSecurityHeadersWebFilter.java @@ -35,19 +35,21 @@ public ViewerSecurityHeadersWebFilter( @Override public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { String path = exchange.getRequest().getPath().value(); - if (!isViewerSurface(path)) { - return chain.filter(exchange); - } + final boolean isViewer = isViewerSurface(path); exchange.getResponse().beforeCommit(() -> { HttpHeaders headers = exchange.getResponse().getHeaders(); - // Avoid caching embedded preview surfaces. + // Avoid caching embedded preview surfaces and API responses. headers.set(HttpHeaders.CACHE_CONTROL, "no-store"); headers.set("X-Content-Type-Options", "nosniff"); headers.set("Referrer-Policy", "no-referrer"); + if (!isViewer) { + return Mono.empty(); + } + // If the response is a redirect, do not attach an error-like CSP that could confuse debugging. if (exchange.getResponse().getStatusCode() == HttpStatus.FOUND) { return Mono.empty(); @@ -75,7 +77,7 @@ public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { }); // Ensure CSP is also applied to HEAD checks for the viewer surface. - if (exchange.getRequest().getMethod() == HttpMethod.HEAD) { + if (isViewer && exchange.getRequest().getMethod() == HttpMethod.HEAD) { return chain.filter(exchange).then(Mono.empty()); } diff --git a/src/test/java/com/clearfolio/viewer/config/ViewerSecurityHeadersWebFilterTest.java b/src/test/java/com/clearfolio/viewer/config/ViewerSecurityHeadersWebFilterTest.java index 0e62117e..7b850e23 100644 --- a/src/test/java/com/clearfolio/viewer/config/ViewerSecurityHeadersWebFilterTest.java +++ b/src/test/java/com/clearfolio/viewer/config/ViewerSecurityHeadersWebFilterTest.java @@ -195,7 +195,8 @@ void doesNotAddHeadersForNonViewerPaths() { HttpHeaders headers = exchange.getResponse().getHeaders(); assertNull(headers.getFirst("Content-Security-Policy")); - assertNull(headers.getFirst("X-Content-Type-Options")); - assertNull(headers.getFirst("Referrer-Policy")); + assertEquals("nosniff", headers.getFirst("X-Content-Type-Options")); + assertEquals("no-store", headers.getFirst(HttpHeaders.CACHE_CONTROL)); + assertEquals("no-referrer", headers.getFirst("Referrer-Policy")); } }