HTTPS everywhere and security headers

HTTP & the web · Advanced · 14 min

A handful of response headers block whole classes of attacks like protocol downgrades, XSS and clickjacking.

Beyond TLS itself, several response headers tell browsers to enforce stricter rules.

Strict-Transport-Security (HSTS): max-age=63072000; includeSubDomains; preload tells the browser to use HTTPS only for this domain for two years, even if a user types http://. That prevents downgrade attacks on public Wi-Fi. Make sure every subdomain supports HTTPS first; it's hard to undo.

Content-Security-Policy (CSP) restricts where scripts, styles, images and connections may come from, which limits the damage of an XSS bug. A strict policy uses nonces: script-src 'nonce-r4nd0m' 'strict-dynamic'; object-src 'none'; base-uri 'none', with the same random nonce on each allowed <script>. Start with Content-Security-Policy-Report-Only to find violations without breaking the page.

Clickjacking: frame-ancestors 'none' in CSP (or the older X-Frame-Options: DENY) stops other sites from embedding yours in an invisible iframe.

Others:

  • X-Content-Type-Options: nosniff: never guess content types.
  • Referrer-Policy: strict-origin-when-cross-origin (the browser default): don't leak full URLs to other sites.
  • Permissions-Policy: camera=(), geolocation=(): disable features you don't use.
  • Cross-Origin-Opener-Policy: same-origin: isolates your window from cross-origin popups.

Combine these with cookies set Secure; HttpOnly; SameSite. Check a site's headers with the browser devtools, or scanners such as Mozilla's HTTP Observatory.

Example

import http from "node:http";
import { randomBytes } from "node:crypto";

http.createServer((req, res) => {
  const nonce = randomBytes(16).toString("base64");
  res.setHeader("Strict-Transport-Security", "max-age=63072000; includeSubDomains");
  res.setHeader("Content-Security-Policy",
    `script-src 'nonce-${nonce}' 'strict-dynamic'; object-src 'none'; ` +
    `base-uri 'none'; frame-ancestors 'none'`);
  res.setHeader("X-Content-Type-Options", "nosniff");
  res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
  res.setHeader("Permissions-Policy", "camera=(), microphone=(), geolocation=()");
  res.setHeader("Content-Type", "text/html; charset=utf-8");
  res.end(`<!doctype html><title>Safe</title>
<script nonce="${nonce}">console.log("allowed")</script>
<script>console.log("blocked by CSP")</script>`);
}).listen(8080);

Remember

  • HSTS forces HTTPS for future visits
  • CSP with nonces limits what scripts can run, containing XSS
  • frame-ancestors 'none' prevents clickjacking
  • Roll out CSP in Report-Only mode first

Watch out for

  • Enabling HSTS with includeSubDomains before every subdomain supports HTTPS.

Check yourself

  1. What does Content-Security-Policy: frame-ancestors 'none' prevent?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP

Builds on

Unlocks

Nothing builds on this yet.