A user uploads a document. The application checks permissions and returns the file. Everything looks correct, but the response says Content-Type: text/html.
The browser treats the file as a web page. If it contains attacker-controlled JavaScript and no effective policy blocks it, that code can run under the application's origin.
HTTP headers help control what the browser can execute, store, and share. They add protection around the content our application delivers. Let's look at the security problems they help solve.
Keep uploaded content from becoming executable code
Content-Type tells the client what kind of content it received:
Content-Type: application/json
Use application/json for JSON, application/pdf for PDFs, and text/plain; charset=utf-8 for plain text. Labeling untrusted content as HTML can turn a file-serving feature into a cross-site scripting problem, usually called XSS. MDN on Content-Type
Pair the correct type with:
X-Content-Type-Options: nosniff
Browsers sometimes inspect content to infer its type. nosniff prevents that guessing and blocks scripts or stylesheets served with an unexpected type. It does not repair a response incorrectly labeled text/html; the server must send the right type first. MDN on nosniff
For a private PDF intended as a download:
Content-Type: application/pdf
Content-Disposition: attachment; filename="report.pdf"
X-Content-Type-Options: nosniff
Cache-Control: no-store
attachment asks the browser to download the file. It does not make a malicious file safe to open. MDN on Content-Disposition
Also, an upload's declared Content-Type comes from the caller. Validate allowed formats, file contents, and size limits on the server. Do not trust that label as proof of what the file contains. OWASP file upload guidance
Restrict scripts with Content Security Policy
Suppose an attacker finds a bug that inserts a script into a page. Content Security Policy, or CSP, can limit which scripts the browser runs and reduce the impact of that injection.
For a simple page that loads external scripts and other resources from its own origin:
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'
Here, 'self' means the page's own origin. The policy restricts resource loading, blocks embedded objects, prevents changes through an HTML base element, and prevents framing. It also blocks inline scripts and styles.
Adapt the policy to the application. A same-origin script allowlist still trusts scripts served from that origin. If inline scripts are necessary, use approved hashes or unpredictable nonces generated for each response. Avoid adding 'unsafe-inline' just to silence errors. MDN CSP guide
Test a new policy using Content-Security-Policy-Report-Only, inspect violations, then enable enforcement. Report-only mode observes violations without blocking them.
CSP belongs on the HTML response. Adding it only to a JSON API does not protect a separately served frontend. Keep output encoding and safe rendering in place too. MDN CSP reference
Prevent clickjacking
An attacker can place our application inside an invisible frame and trick someone into clicking a real button, such as Delete, while showing a different interface. This is clickjacking.
The CSP directive from the previous example prevents embedding:
Content-Security-Policy: frame-ancestors 'none'
Keep it inside the complete CSP policy. X-Frame-Options: DENY provides a fallback for browsers that do not support frame-ancestors. If legitimate embedding is required, allow only the origins that need it. OWASP framing guidance
Disable browser features the page does not need
A document dashboard probably does not need the camera, microphone, or the user's location. Make those limits explicit on the HTML response:
Permissions-Policy: camera=(), microphone=(), geolocation=()
The empty allowlist () disables each feature for the page and its embedded frames, where supported. This reduces the browser capabilities available to code running there, including third-party scripts.
Choose restrictions that fit the application and check support in your target browsers. Permissions Policy controls specific features; it does not make untrusted scripts safe or replace CSP. MDN on Permissions-Policy
Keep private responses out of caches
Alice's private report must not become a response that a shared cache serves to Bob. For sensitive account pages and document responses, use:
Cache-Control: no-store
This tells private and shared HTTP caches not to store the response. Two similar-looking settings mean different things:
| Setting | Meaning |
|---|---|
no-store |
Do not store the response in HTTP caches |
no-cache |
Storage is allowed, but reuse requires validation |
private |
Storage is allowed in a private cache, such as the browser's, but not a shared cache |
Check that CDN rules respect the policy. Cache headers do not replace authorization or remove copies someone already downloaded. MDN on Cache-Control
Keep connections on HTTPS
A redirect moves an HTTP request to HTTPS. HTTP Strict Transport Security, or HSTS, tells the browser to use HTTPS for future connections to the host:
Strict-Transport-Security: max-age=300
This short example lasts five minutes. Start with a small duration during rollout, then increase it after confirming HTTPS works reliably. Browsers accept HSTS only over HTTPS.
Add includeSubDomains only when all affected subdomains support HTTPS. Without an existing HSTS policy or preload entry, the first HTTP visit remains exposed before the redirect. MDN on HSTS
Protect the session cookie
A session cookie identifies the user's authenticated session. Its response header should define how the browser handles it:
Set-Cookie: session=opaque-value; Path=/; Secure; HttpOnly; SameSite=Lax
Securerestricts transmission to secure connections.HttpOnlyprevents JavaScript from reading the cookie through browser cookie APIs.SameSite=Laxlimits when the browser includes it in requests from other sites, while allowing some navigations.
HttpOnly makes cookie theft harder, but injected JavaScript can still make authenticated requests. SameSite adds CSRF protection; it does not replace the application's CSRF defenses. MDN on Set-Cookie
Clean up browser data after logout
Alice signs out on a shared computer. Ending her session does not automatically remove report data the application stored in the browser.
After invalidating the session on the server, the successful logout response over HTTPS can include:
Clear-Site-Data: "cache", "cookies", "storage"
This asks the browser to clear the selected data types. "storage" includes localStorage, IndexedDB, and service worker registrations for the response's origin. Browser cleanup complements server logout; it does not invalidate a session by itself.
Choose the scope deliberately: "cookies" affects the entire registered domain, including subdomains, and clearing storage can remove preferences or offline work. Use it for the intended logout flow and check browser support for the selected directives. MDN on Clear-Site-Data
Limit what other sites can read
CORS headers tell the browser which other origins may read responses from our API:
Access-Control-Allow-Origin: https://dashboard.example.com
Access-Control-Allow-Credentials: true
Vary: Origin
For requests using cookies, allow a specific trusted origin rather than *. If selecting origins dynamically, validate them against an allowlist and return Vary: Origin. Configure preflight permissions for the required methods and headers too.
CORS is not authentication or authorization. A command-line client does not need the browser's permission to call an API, and some unwanted browser requests can reach the server even when their responses cannot be read. MDN CORS guide
For more detail, see Understanding CSRF and CORS.
For sensitive pages that should not disclose their URL through the referrer header, use Referrer-Policy: no-referrer. Keep secrets out of URLs regardless. MDN on Referrer-Policy
Configure the response that reaches the browser
Spring Security provides defaults including nosniff, frame protection, cache prevention, and HSTS on requests recognized as secure. CSP needs an application-specific policy.
Inside the existing SecurityFilterChain configuration, before http.build(), add:
http.headers(headers -> headers
.contentSecurityPolicy(csp -> csp.policyDirectives(
"default-src 'self'; script-src 'self'; "
+ "object-src 'none'; base-uri 'none'; "
+ "frame-ancestors 'none'"))
);
This Spring Security 6.5 example keeps the other header defaults. Adjust the policy to the resources the page needs. Spring Security header configuration
If a separate web server serves the frontend, configure the HTML headers there too. Choose where each policy is set and inspect the final response after proxies and CDNs.
Use browser developer tools to check an authenticated page, a download, and an error response. Confirm that private content uses the intended cache policy and that CSP blocks unexpected scripts without breaking legitimate features.
Also check that restricted browser features are unavailable and that a successful logout clears the intended browser data while ending the server session.
