skip to content

How do you configure Content-Security-Policy in Spring Security, why isn't it on by default, and how does report-only mode work?

level: seniorimportance: should knowfreq 45%

answer

  1. opt-in, not default (app-specific)
  2. policyDirectives(...) → ContentSecurityPolicyHeaderWriter
  3. reportOnly() → CSP-Report-Only header
  4. 'unsafe-inline' defeats it; use nonce/hash
  5. frame-ancestors = modern X-Frame-Options

basics

~10 s

CSP restricts which sources a page may load scripts/styles/etc. from, mitigating XSS. It isn't a Spring default because policies are app-specific. Configure it with headers().contentSecurityPolicy(csp -> csp.policyDirectives("default-src 'self'")).

solid answer

~40 s

Content-Security-Policy is an allow-list header telling the browser which origins may supply scripts, styles, images, frames, etc., strongly mitigating XSS and data injection. Spring Security does NOT send it by default because a correct policy is entirely app-specific — a wrong global default would break most apps. You opt in through the headers DSL: `.contentSecurityPolicy(csp -> csp.policyDirectives("default-src 'self'; script-src 'self'; object-src 'none'"))`, which writes `Content-Security-Policy`. For safe rollout, use `.contentSecurityPolicy(csp -> csp.policyDirectives("...").reportOnly())`, which instead sends `Content-Security-Policy-Report-Only`: the browser reports violations (to a `report-uri`/`report-to` endpoint or the console) without blocking anything, so you can tune the policy before enforcing. CSP also carries `frame-ancestors`, the modern replacement for X-Frame-Options. The hard part is eliminating inline scripts/styles or using nonces/hashes, since `'unsafe-inline'` largely defeats the protection.

code

java · 15 lines
java
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.headers(headers -> headers
        .contentSecurityPolicy(csp -> csp
            .policyDirectives(
                "default-src 'self'; " +
                "script-src 'self'; " +
                "style-src 'self'; " +
                "object-src 'none'; " +
                "frame-ancestors 'self'; " +
                "report-uri /csp-reports")
            // Roll out safely: report first, don't block, then remove .reportOnly()
            .reportOnly()));
    return http.build();
}

go deeper

for a junior

Know CSP restricts resource sources and is opt-in via policyDirectives.

for a middle

Write a basic policy and explain default-src/script-src.

for a senior

Use report-only rollout and understand the inline-script/nonce problem.

for a principal

Design a nonce strategy and staged report-to-enforce migration across services.

## What CSP is **Content-Security-Policy** is a response header that gives the browser an allow-list of trusted sources for each type of resource. If the page tries to load a script from an origin not on the list, the browser blocks it. Its primary value is **mitigating cross-site scripting (XSS)**: even if an attacker injects `<script src=evil.com>`, the browser refuses to run it because `evil.com` isn't allowed. ## Anatomy of a policy A policy is a `;`-separated list of **directives**, each naming a resource type and its allowed sources: - `default-src 'self'` — fallback for everything; only same-origin. - `script-src`, `style-src`, `img-src`, `font-src`, `connect-src`, `frame-src` — per-type overrides. - `object-src 'none'` — block plugins. - `frame-ancestors 'self'` — who may frame *this* page (the modern X-Frame-Options). - `report-uri` / `report-to` — where the browser POSTs violation reports. Source keywords: `'self'`, `'none'`, `'unsafe-inline'`, `'unsafe-eval'`, `'nonce-<base64>'`, `'sha256-<hash>'`, and explicit origins like `https://cdn.example.com`. ## Why Spring Security won't default it Unlike nosniff or DENY, there is no universally-safe CSP. Any non-trivial app loads scripts/styles from somewhere, and a default of `default-src 'self'` would break most pages (inline scripts, CDNs, analytics). So Spring makes CSP **opt-in**. ## Configuring in Spring Security ```java http.headers(h -> h.contentSecurityPolicy(csp -> csp.policyDirectives("default-src 'self'; script-src 'self'; object-src 'none'; frame-ancestors 'self'"))); ``` This is written by `ContentSecurityPolicyHeaderWriter`. ## Report-only mode Rolling out CSP blind will break the site. Instead: ```java http.headers(h -> h.contentSecurityPolicy(csp -> csp.policyDirectives("default-src 'self'; report-uri /csp-reports").reportOnly())); ``` `.reportOnly()` emits the header **`Content-Security-Policy-Report-Only`** instead of the enforcing one. The browser does NOT block anything; it just sends violation reports to your `report-uri`/`report-to` endpoint (and logs to console). You collect these, refine the policy until reports stop, then remove `.reportOnly()` to enforce. You can even run both headers simultaneously (enforce a loose policy, report-only a stricter one). ## The inline-script problem CSP's power collapses if you allow `'unsafe-inline'` for scripts, because injected inline `<script>` then runs freely. Proper hardening removes inline handlers/styles or authorizes them with per-response **nonces** (`'nonce-...'`) or **hashes** (`'sha256-...'`). Spring Security's `policyDirectives` is a static string; if you need per-request nonces you generate them yourself (e.g. a filter/`HeaderWriter` or template integration) and inject the value into both the header and the `<script nonce>` attribute. ## Gotchas - `'unsafe-inline'` / `'unsafe-eval'` largely defeat the point — avoid. - `report-uri` is deprecated in favor of `report-to` + a `Report-To` header, though browsers still accept `report-uri`. - CSP `frame-ancestors` overrides X-Frame-Options in modern browsers; keep them consistent. - A too-strict policy silently breaks fonts/images/XHR — test with report-only first. - CSP is not a substitute for output encoding; it's defense-in-depth on top of proper escaping.

  • Why does adding 'unsafe-inline' to script-src undermine CSP's XSS protection?
    'unsafe-inline' permits ANY inline <script> or event-handler attribute to execute, which is exactly the injection vector XSS uses. The browser can no longer distinguish attacker-injected inline script from legitimate ones. Use per-response nonces ('nonce-...') or hashes ('sha256-...') instead.
  • How would you roll out a strict CSP without breaking a live production app?
    Deploy it in report-only mode via .reportOnly(), which sends Content-Security-Policy-Report-Only. The browser reports violations to your report-uri/report-to endpoint without blocking. Collect and analyze reports, tighten allowed sources until violations stop, then switch to enforcing by removing reportOnly().

saying these in an interview costs you the question

  • Assuming Spring Security sends a CSP by default
  • Adding 'unsafe-inline' and calling the app CSP-protected
  • Thinking report-only mode blocks requests
  • Believing CSP replaces server-side output encoding

context