WEB SECURITY
HTTP security headers, explained
"Add security headers" is the most common recommendation in any web security scan, and one of the most commonly half-implemented. Each header defends against a specific attack, and a couple of them will break your site if you set them carelessly. Here is what each one does, how to check what you are already sending, and a starting configuration you can adapt.
First, see what you send today
Before changing anything, look at your current response headers:
curl -sSI https://yoursite.example.com/ | grep -iE \
'strict-transport|content-security|x-frame|x-content-type|referrer|permissions'
Whatever is missing from that output is a gap. Now, header by header.
Strict-Transport-Security (HSTS)
Tells the browser to only ever contact your site over HTTPS, even if a user types
http:// or clicks an old link. This closes the window where a first, plaintext
request can be intercepted and downgraded.
Strict-Transport-Security: max-age=31536000; includeSubDomains
max-age seconds — so do not send it until HTTPS works everywhere, including every
subdomain if you add includeSubDomains. Start with a short max-age while
you confirm nothing breaks, then raise it.Content-Security-Policy (CSP)
The most powerful header here, and the fiddliest. CSP tells the browser which sources of
scripts, styles, images, and other resources are allowed to load — so even if an attacker injects
a <script> tag, the browser refuses to run it. It is the strongest defense
against cross-site scripting there is.
A strict starting point for a site that loads only its own assets:
Content-Security-Policy: default-src 'self';
img-src 'self' data:;
style-src 'self' 'unsafe-inline';
script-src 'self';
base-uri 'self';
frame-ancestors 'none'
The hard part is 'unsafe-inline'. Every inline <script> and
inline event handler (onclick="...") is blocked by script-src 'self',
which is exactly the point — but it means a page with inline scripts will silently stop working.
Move that JavaScript into external files. Inline styles are more often unavoidable
(many UIs set style="width:..." dynamically), which is why the example allows
'unsafe-inline' for styles but not scripts.
CSP fails silently — a blocked resource just does not load, with no visible error unless you are
watching the console. Add a report-uri or test in report-only mode
(Content-Security-Policy-Report-Only) before enforcing.
X-Frame-Options
Stops other sites from loading yours inside an <iframe>, which is how
clickjacking works — an invisible frame of your site laid over a decoy the victim thinks they are
clicking. DENY blocks all framing:
X-Frame-Options: DENY
The modern equivalent is the CSP frame-ancestors 'none' directive, which is more
flexible; sending both covers older browsers too.
X-Content-Type-Options
Stops the browser from "sniffing" a response's content type and second-guessing your declared
Content-Type. Without it, a file you serve as text could be interpreted as script.
There is only one value, and every site should send it:
X-Content-Type-Options: nosniff
Referrer-Policy
Controls how much of the current URL is sent in the Referer header when a user
navigates away. This matters when your URLs contain anything sensitive — tokens, IDs, search
terms. A good default sends the full URL within your own site but only the origin to other sites:
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy
Declares which browser features — camera, microphone, geolocation, and others — the page is allowed to use. Disabling what you do not need shrinks the damage a script injection could do:
Permissions-Policy: geolocation=(), microphone=(), camera=()
A starting configuration
Putting it together, here is a reasonable baseline for a static or server-rendered site. On
Netlify or Cloudflare Pages this goes in a _headers file; on nginx or Apache it maps
to add_header / Header set directives:
/*
Strict-Transport-Security: max-age=31536000
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), microphone=(), camera=()
Content-Security-Policy: default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self'; base-uri 'self'; frame-ancestors 'none'
Adapt the CSP to your real dependencies: if you load fonts, analytics, or a payment widget from
another domain, you must add those hosts to the relevant directive or the browser will block them.
That is the whole point of the header — so add hosts deliberately, one at a time, rather than
loosening it to *.
When you are done, re-run the curl check from the top of this page against your
live site to confirm every header is actually being sent.