Skip to content
All posts

Security Headers Explained: CSP, HSTS, and What Scanners Check

5 min readsecurity headers · web security · CSP · HSTS · automated scanning

Security Headers Explained: CSP, HSTS, and What Scanners Check

HTTP security headers are some of the cheapest, highest-leverage defenses you can ship. They are short response directives the browser already knows how to enforce, so a few lines of server configuration can shut down entire classes of attacks: clickjacking, cross-site scripting (XSS), protocol downgrade, MIME sniffing, referrer leakage, and cross-origin data exposure.

The catch is that security headers are easy to get subtly wrong. A Content-Security-Policy with unsafe-inline looks protective but isn't. An HSTS header without preload still leaves the very first request exposed. This guide walks through the headers that matter, the recommended values, the mistakes that quietly defeat them, and how automated scanning keeps them honest over time.

Why security headers matter

Application code can be flawless and you can still be exploited because of how the browser treats your responses. Headers move part of your security policy out of your code and into the browser's enforcement engine. They are declarative, framework-agnostic, and apply uniformly to every asset on the page.

They are not a silver bullet. Headers complement input validation, output encoding, and authentication — they do not replace them. But a strong header baseline raises the cost of attack dramatically, which is why frameworks like the OWASP Top 10 (2025) treat their absence as a finding worth flagging.

The headers that matter

Content-Security-Policy (CSP)

CSP is the most powerful and the most error-prone header. It tells the browser which sources of script, style, image, and other content are allowed to load, which neutralizes most injected-script attacks even when an XSS bug exists.

A modern, strict policy leans on nonces or hashes rather than host allowlists:

Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-r4nd0m'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'

The most common mistake is script-src 'self' 'unsafe-inline'. The moment you allow unsafe-inline, an attacker who can inject a <script> tag wins, and the rest of your policy provides little real protection. unsafe-eval is similarly dangerous. Prefer nonces or hashes, and add object-src 'none' and base-uri 'none' to close common bypasses.

Strict-Transport-Security (HSTS)

HSTS forces browsers to use HTTPS for your domain, defeating protocol-downgrade and SSL-stripping attacks. A solid value:

Strict-Transport-Security: max-age=63072000; includeSubDomains; preload

The max-age (here two years) should be long. Two frequent mistakes: a max-age that is too short to be meaningful, and omitting preload. Without preload, the very first visit to your site — before any HSTS header has been seen — can still be downgraded. Preloading bakes your domain into the browser's built-in list so even that first request is protected. Only add includeSubDomains and preload once every subdomain truly serves HTTPS.

X-Frame-Options and frame-ancestors

These stop clickjacking, where your page is loaded in an invisible frame on an attacker's site. The modern control is the CSP frame-ancestors directive; X-Frame-Options remains useful for older browsers.

X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'none'

Use frame-ancestors 'self' if you legitimately frame your own pages.

X-Content-Type-Options

This single value stops the browser from MIME-sniffing a response into a different content type than you declared, a vector for tricking the browser into executing non-script files as script.

X-Content-Type-Options: nosniff

There is no downside; it should always be present.

Referrer-Policy

Without a policy, browsers may leak full URLs — including tokens or IDs in query strings — to third-party sites via the Referer header. A sensible default:

Referrer-Policy: strict-origin-when-cross-origin

This sends the full URL within your own origin but only the origin (no path or query) cross-site, and nothing when downgrading from HTTPS to HTTP.

Permissions-Policy

This header restricts access to powerful browser features such as camera, microphone, and geolocation. Lock down what you don't use:

Permissions-Policy: camera=(), microphone=(), geolocation=()

Cross-origin isolation: COOP, COEP, CORP

These headers isolate your document from cross-origin windows and resources. They mitigate cross-origin leak attacks (such as Spectre-style side channels) and are required to enable powerful APIs like SharedArrayBuffer.

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Resource-Policy: same-origin

Enable these carefully — require-corp can break legitimate cross-origin embeds that don't opt in.

Quick reference

Header Protects against Recommended value
Content-Security-Policy XSS, injection, clickjacking default-src 'self'; script-src 'self' 'nonce-...'; object-src 'none'; frame-ancestors 'none'
Strict-Transport-Security Protocol downgrade, SSL stripping max-age=63072000; includeSubDomains; preload
X-Frame-Options Clickjacking (legacy) DENY
X-Content-Type-Options MIME sniffing nosniff
Referrer-Policy Referrer leakage strict-origin-when-cross-origin
Permissions-Policy Feature abuse camera=(), microphone=(), geolocation=()
Cross-Origin-Opener-Policy Cross-origin window attacks same-origin
Cross-Origin-Embedder-Policy Cross-origin resource leaks require-corp
Cross-Origin-Resource-Policy Cross-site resource theft same-origin

Common mistakes to avoid

How automated scanning verifies headers

Getting headers right once is the easy part. Keeping them right across deploys, new endpoints, CDN changes, and framework upgrades is where teams drift. A config change, a proxy in front of your app, or a new route can silently drop a header without anyone noticing.

This is where continuous, automated scanning earns its place. At NANOTESTING, every web scan checks security headers and cookie flags as part of the non-invasive baseline. It evaluates HSTS (including max-age), Content-Security-Policy (flagging weak directives like unsafe-inline), X-Frame-Options and frame-ancestors, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, the COOP/COEP/CORP isolation headers, and cookie Secure/HttpOnly/SameSite flags. Findings map to the OWASP Top 10 (2025) and other frameworks and roll up into an A–F risk grade.

One principle worth calling out: when a check can't be performed, it is reported honestly as a coverage gap — never quietly counted as a clean pass. A truthful "we couldn't verify this" is far more useful than a green checkmark you can't trust.

Conclusion

Security headers are a high-return investment: a handful of directives that let the browser enforce policy on your behalf against clickjacking, XSS, downgrade attacks, and cross-origin leaks. Start with a strict CSP built on nonces, a long HSTS with preload, nosniff, a sensible Referrer-Policy, and locked-down cross-origin isolation — then pair strong security headers with Secure, HttpOnly, SameSite cookies. Configure them carefully, and lean on continuous automated scanning so a future deploy doesn't quietly undo the protection you worked to add.

Related posts