skip to content

Which HTTP security response headers does Spring Security add by default, and where does that behavior come from?

level: juniorimportance: must knowfreq 70%

answer

  1. HeadersConfigurer = secure defaults
  2. nosniff + DENY + no-store + HSTS(https)
  3. CSP & Referrer-Policy opt-in only
  4. X-XSS-Protection now '0'
  5. http.headers(...) DSL

basics

~10 s

By 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 s

Out 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
java
@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

for a junior

Know the four default headers by name and that they're free/automatic.

for a middle

Explain each header's purpose and that CSP/Referrer-Policy are opt-in.

for a senior

Discuss the HeadersConfigurer/HeaderWriter mechanism and the X-XSS-Protection=0 change.

for a principal

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

context