Security headers: HSTS, CSP, X-Frame-Options and others
· 7 min read
The backend is a fortress, but the other half of your defense lives in the browser. HSTS, CSP and other headers block XSS, clickjacking and MITM - for free.
Why they matter
An application can be perfectly secure on the backend and still be vulnerable to browser-side attacks: cross-site scripting (XSS), clickjacking, protocol downgrade, MIME confusion. Security headers push the defense down into the browser itself.
Strict-Transport-Security (HSTS)
Forces HTTPS for all future visits. After the first HTTPS visit the browser remembers the domain and rewrites any http:// link to https:// on its own, even if the user clicks a bad link.
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
max-age=63072000= valid for 2 yearsincludeSubDomains= the rule applies to all subdomains too; it forces HTTPS everywhere, so if you have a subdomain running over HTTP only, e.g.staging.example.com, it becomes unreachable in the browserpreload= allows inclusion in the HSTS preload list built into Chrome / Firefox / Safari
Caution:
HSTS with preload takes roughly 3-4 months (a full browser release cycle) to remove from the list. Don't add preload until you have verified that HTTPS works reliably on all subdomains.
Content-Security-Policy (CSP)
The most powerful header, but also the hardest to configure. It whitelists where the browser may load scripts, styles, images and iframes from. Without CSP, an attacker who can inject a <script> tag can run arbitrary JS; with CSP, only code from approved sources runs.
Content-Security-Policy: default-src 'self'; script-src 'self' 'sha256-...'; img-src 'self' data: https://cdn.example.com; style-src 'self' 'unsafe-inline'; frame-ancestors 'none'
Common directives:
default-src 'self'= by default allow only resources from my own domainscript-src= JS sources (specify a hash or nonce for inline scripts)style-src= CSS sourcesimg-src= imagesconnect-src= AJAX, WebSocket, EventSourceframe-ancestors 'none'= nobody may embed the page in an iframe (better than X-Frame-Options)report-uri /csp-report= the browser sends JSON for every violation (you catch it in the backend and log it)- Note:
report-uriis now deprecated - browsers that supportreport-toignore it. The modern approach is thereport-todirective plus a separateReporting-Endpointsheader; keepreport-urionly as a fallback for Firefox, which does not supportreport-toyet.
X-Frame-Options
An older alternative to frame-ancestors. Prevents clickjacking, where an attacker embeds your page as an iframe and overlays invisible buttons on top of it.
X-Frame-Options: DENY
Values: DENY (nobody), SAMEORIGIN (only my domain), ALLOW-FROM uri (deprecated).
X-Content-Type-Options
X-Content-Type-Options: nosniff
Disables MIME sniffing, so the browser respects the Content-Type the server returned. Without it, an attacker can upload a file with the wrong type (e.g. an image that is actually HTML with a script) and the browser may run it as a web page.
Referrer-Policy
Referrer-Policy: strict-origin-when-cross-origin
Controls how much information about the previous page the browser sends in the Referer header on a click. The default in modern browsers is already strict-origin-when-cross-origin, but setting the header explicitly guarantees consistent behavior.
Permissions-Policy
Disables APIs you don't need, such as camera, microphone, GPS and geolocation. An attacker exploiting XSS cannot request these APIs.
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
Practical nginx configuration
server {
listen 443 ssl http2;
server_name example.com;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
# we deliberately left out preload here - add it only when you are 100% sure (see the preload trap above)
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
# CSP is extensive - define it according to your application
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; ..." always;
}
The key is always - without it, nginx omits the headers on error responses (4xx, 5xx).
Auditing the current state
The easiest way is an online tool. Our header check shows you which headers are missing and which are wrong. For a thorough audit, securityheaders.com also gives an A-F score.
Conclusion
Security headers are one of the best investments in security by effort-to-payoff ratio: typically half an hour of setup in a reverse proxy against a whole category of browser-side attacks. An audit once every six months as part of regular maintenance is a reasonable minimum.
Free security headers audit
No registration, result in three seconds.
Try ePulz.io free - 7 days, no credit card needed.
Create account