Default security headers
Add Spring Security and you get these on every response, with no configuration. Read from Spring Security’s reference documentation:
Cache-Control: no-cache, no-store, max-age=0, must-revalidatePragma: no-cacheExpires: 0X-Content-Type-Options: nosniffStrict-Transport-Security: max-age=31536000 ; includeSubDomainsX-Frame-Options: DENYX-XSS-Protection: 0Rated low because there is nothing to add. The exposure this page closes is the case
where somebody replaced the defaults instead of extending them.
// Extend the defaults. Do not start from an empty headers() block.http.headers(headers -> headers .frameOptions(frame -> frame.sameOrigin()) // only if you genuinely frame .contentSecurityPolicy(csp -> csp .policyDirectives("default-src 'self'; object-src 'none'")));curl -sI https://example.com/ | grep -iE 'cache-control|x-content-type|strict-transport|x-frame|x-xss'Three things worth knowing before you touch them
Section titled “Three things worth knowing before you touch them”HSTS is one year, and it is only sent over HTTPS. The documentation notes “Strict-Transport-Security is added only on HTTPS requests”, which is why it often appears absent in local testing and present in production. A browser that sees it refuses plain HTTP for this host and every subdomain for a year and will not forget on request, so confirm every subdomain serves HTTPS before shipping it. This is the same trap as helmet’s default.
X-XSS-Protection: 0 is deliberate. Spring Security’s docs say it “instructs
browsers to disable the XSS Auditor”. Setting it to 1; mode=block — which plenty of
hardening checklists still ask for — reintroduces a filter that browsers removed
because it could be manipulated into disabling a page’s legitimate scripts.
No CSP by default. The one header that would do the most is not in the list; you have to add it. That is the real gap on this page.
X-Frame-Options defaults to DENY here — stricter than
helmet’s SAMEORIGIN and matching
Django’s default. If you frame your own pages, sameOrigin()
is the deliberate loosening; the H2 console in particular will not render without it,
which is one of the more common reasons this gets changed.
The X-XSS-Protection consensus
Section titled “The X-XSS-Protection consensus”This is now corroborated four independent ways, which is worth having to hand when a compliance scanner flags the missing header:
- Spring Security sets it to
0by default. - helmet sets it to
0by default. - Django removed the setting entirely in 4.0.
- Browsers removed the filter rather than fix it — the upstream reason all three frameworks agree.
Three maintained frameworks and the browser vendors, one answer.
The failure mode here is subtraction, not addition. A headers() block written to add
one header can drop the rest if it disables defaults, and nothing warns — the
application works, the headers are simply gone.
frameOptions().sameOrigin() or .disable() is the usual first step down that path.
Check the full response header set after any change to this block, not just the header
you meant to change.
Adding a CSP breaks inline scripts and styles, as everywhere else. Start in report-only.
Related
Section titled “Related”- SecurityFilterChain — where the headers block lives
- Application vs server — if a proxy also sets these headers, decide which layer owns them