Back to blog

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.

Security headers: HSTS, CSP, X-Frame-Options and others

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 years
  • includeSubDomains = 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 browser
  • preload = 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 domain
  • script-src = JS sources (specify a hash or nonce for inline scripts)
  • style-src = CSS sources
  • img-src = images
  • connect-src = AJAX, WebSocket, EventSource
  • frame-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-uri is now deprecated - browsers that support report-to ignore it. The modern approach is the report-to directive plus a separate Reporting-Endpoints header; keep report-uri only as a fallback for Firefox, which does not support report-to yet.

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.

Test the website →

Share: Link copied

Try ePulz.io free - 7 days, no credit card needed.

Create account