Which HTTP security response headers does Spring Security add by default, and where does that behavior come from?
answer
- HeadersConfigurer = secure defaults
- nosniff + DENY + no-store + HSTS(https)
- CSP & Referrer-Policy opt-in only
- X-XSS-Protection now '0'
- http.headers(...) DSL
basics
~10 sBy default Spring Security adds protective response headers automatically: Cache-Control (no-store), X-Content-Type-Options: nosniff, X-Frame-Options: DENY, and Strict-Transport-Security (HSTS) on HTTPS requests. You don't configure anything to get them.
solid answer
~40 sOut of the box, Spring Security applies a secure-by-default set of response headers via HeadersConfigurer (configured through http.headers(...)). The defaults are: Cache-Control/Pragma/Expires that disable caching, X-Content-Type-Options: nosniff (stops MIME sniffing), X-Frame-Options: DENY (anti-clickjacking), and Strict-Transport-Security (HSTS) added only on secure/HTTPS requests. In Spring Security 6 the X-XSS-Protection header is written as '0' (disabled) per OWASP guidance since the legacy filter caused vulnerabilities. Notably, Content-Security-Policy and Referrer-Policy are NOT sent by default — you opt in explicitly. Each writer can be tuned or disabled individually inside the headers DSL. These headers ship the moment Spring Security is on the classpath with a filter chain, so they're a free baseline of browser-side hardening you inherit without writing code.
code
java · 16 lines@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// These defaults are already ON without this block; shown for clarity.
http.headers(headers -> headers
.contentTypeOptions(withDefaults()) // X-Content-Type-Options: nosniff
.frameOptions(withDefaults()) // X-Frame-Options: DENY
.cacheControl(withDefaults()) // no-cache/no-store/...
.httpStrictTransportSecurity(withDefaults()) // HSTS on HTTPS
);
return http.build();
}
}go deeper
Know the four default headers by name and that they're free/automatic.
Explain each header's purpose and that CSP/Referrer-Policy are opt-in.
Discuss the HeadersConfigurer/HeaderWriter mechanism and the X-XSS-Protection=0 change.
Reason about proxy/forwarded-headers effects on HSTS and static-asset caching trade-offs.
## What are HTTP security headers? HTTP response headers are metadata the server sends alongside a page. A handful of them instruct the *browser* to enforce security rules the server cannot enforce alone — blocking framing, MIME sniffing, insecure transport, caching of sensitive pages, etc. They are a **defense-in-depth** layer: the browser is the enforcement point. ## Where Spring Security produces them Spring Security wires a servlet filter (`HeaderWriterFilter`) that runs a set of `HeaderWriter` implementations. You configure it through the `HttpSecurity` DSL: `http.headers(headers -> ...)`, backed by `HeadersConfigurer`. The key point for a junior: **you get a secure baseline with zero configuration** the moment a `SecurityFilterChain` exists. ## The default headers (Spring Security 6) - **Cache-Control: no-cache, no-store, max-age=0, must-revalidate** (plus `Pragma: no-cache` and `Expires: 0`). Prevents caching of authenticated pages in shared/back-button caches. Written by `CacheControlHeadersWriter`. - **X-Content-Type-Options: nosniff** — tells the browser not to guess ("sniff") a response's content type, which blocks attacks where e.g. an uploaded file is interpreted as executable script. - **X-Frame-Options: DENY** — the page may not be embedded in any `<frame>`/`<iframe>`, defeating **clickjacking**. - **Strict-Transport-Security (HSTS)** — `max-age=31536000 ; includeSubDomains`. Tells the browser to only ever use HTTPS for this host. **Only added on secure requests** (when `request.isSecure()` is true), because sending it over plain HTTP is meaningless. - **X-XSS-Protection: 0** — the header is still written but with value `0`, i.e. the legacy browser XSS auditor is explicitly **disabled**. This changed in Spring Security 5.8/6.0 following OWASP: the old `1; mode=block` value could itself introduce vulnerabilities and modern browsers removed the feature. ## What is NOT sent by default - **Content-Security-Policy (CSP)** — powerful but app-specific, so you must opt in. - **Referrer-Policy** — also opt-in. ## How to change them Everything lives inside `http.headers(...)`. Examples: `.frameOptions(f -> f.sameOrigin())`, `.contentSecurityPolicy(csp -> csp.policyDirectives("..."))`, `.cacheControl( CacheControlConfig::disable )`, or `.httpStrictTransportSecurity(hsts -> hsts.maxAgeInSeconds(...))`. You can disable the whole block with `.headers(HeadersConfigurer::disable)` (rarely a good idea). ## Gotchas - HSTS silently absent in local/HTTP dev — that's expected, not a bug. - Cache-Control no-store can break serving of cacheable static assets if your security filter chain covers them; scope the chain or relax caching for static paths. - Behind a TLS-terminating proxy the request may look non-secure to the app, so HSTS won't be added unless forwarded-headers handling is configured.
- Why isn't the Strict-Transport-Security header showing up in my local dev environment?HSTS is only written on secure (HTTPS) requests by default. Over plain HTTP `request.isSecure()` is false, so `HstsHeaderWriter` skips it — which is correct, since HSTS over HTTP would be ignored/meaningless anyway.
- Are Content-Security-Policy and Referrer-Policy part of the defaults?No. Both must be explicitly configured via `.contentSecurityPolicy(...)` and `.referrerPolicy(...)`. They're app-specific enough that Spring won't guess a policy for you.
saying these in an interview costs you the question
- Claiming Content-Security-Policy is sent by default
- Saying HSTS is added on every request including plain HTTP
- Thinking you must write config to get any security headers at all
- Believing X-XSS-Protection default is '1; mode=block' in Spring Security 6