Skip to content

Default security headers

Severity: lowApplies to: Spring Security 6.xApplies to: Spring Security 7.xFacts last verified 2026-07-26 against Spring Security 7.1.0

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-revalidate
Pragma: no-cache
Expires: 0
X-Content-Type-Options: nosniff
Strict-Transport-Security: max-age=31536000 ; includeSubDomains
X-Frame-Options: DENY
X-XSS-Protection: 0

Rated low because there is nothing to add. The exposure this page closes is the case where somebody replaced the defaults instead of extending them.

the fix
// 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'"))
);
verify it workedRun this in: http response
Terminal window
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.

This is now corroborated four independent ways, which is worth having to hand when a compliance scanner flags the missing header:

Three maintained frameworks and the browser vendors, one answer.

before you ship this

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.