DEV Community

Weather Clock Dash
Weather Clock Dash

Posted on

HTTP Security Headers: CSP, HSTS, and the Essentials Every App Needs

HTTP security headers are one of the highest-ROI security improvements you can make to a web app. They're a few lines of server config that defend against XSS, clickjacking, MIME sniffing, and protocol downgrade attacks. Yet most production apps still don't use them.

This guide covers the essential security headers, what they do, and what to actually set.

Content-Security-Policy (CSP)

CSP is the most powerful and most complex security header. It tells the browser which sources are allowed to load scripts, styles, images, and other resources — blocking inline scripts and unknown CDNs that attackers might inject.

A basic strict CSP:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-RANDOM_NONCE';
  style-src 'self' 'nonce-RANDOM_NONCE';
  img-src 'self' data: https:;
  font-src 'self';
  connect-src 'self' https://api.example.com;
  frame-ancestors 'none';
  upgrade-insecure-requests;
Enter fullscreen mode Exit fullscreen mode

Key directives:

  • default-src 'self' — baseline: only load from your own origin
  • script-src 'nonce-...' — scripts need a per-request nonce attribute; blocks injected scripts
  • frame-ancestors 'none' — prevents your site from being iframed (clickjacking defense)
  • upgrade-insecure-requests — rewrites http:// to https:// for subresources

Nonces in practice:

// Express: generate nonce per request
const crypto = require('crypto');

app.use((req, res, next) => {
  res.locals.nonce = crypto.randomBytes(16).toString('base64');
  res.setHeader('Content-Security-Policy',
    `script-src 'nonce-${res.locals.nonce}';`
  );
  next();
});

// Template: use the nonce
// <script nonce="<%= nonce %>">...</script>
Enter fullscreen mode Exit fullscreen mode

Test mode before enforcing:

Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report
Enter fullscreen mode Exit fullscreen mode

This logs violations without blocking anything. Invaluable for finding what you'd break before switching to enforce mode.

Strict-Transport-Security (HSTS)

Tells the browser to only connect over HTTPS — even if the user types http://:

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Enter fullscreen mode Exit fullscreen mode
  • max-age=31536000 — remember for 1 year
  • includeSubDomains — applies to all subdomains
  • preload — submit to browser preload lists so HTTPS is enforced before the first visit

Before enabling: Make sure your entire site (including all subdomains) works over HTTPS. HSTS with includeSubDomains is hard to undo — the browser won't connect over HTTP until the max-age expires.

Start with a short max-age (like 3600), verify everything works, then increase to 31536000.

X-Frame-Options

Prevents your pages from being embedded in iframes — clickjacking defense:

X-Frame-Options: DENY
Enter fullscreen mode Exit fullscreen mode

Or allow only same-origin iframes:

X-Frame-Options: SAMEORIGIN
Enter fullscreen mode Exit fullscreen mode

Note: frame-ancestors in CSP supersedes this header and is more flexible, but X-Frame-Options is still needed for older browsers.

X-Content-Type-Options

Prevents MIME type sniffing — a category of attack where the browser ignores your Content-Type and guesses the file type, potentially executing a file as JavaScript:

X-Content-Type-Options: nosniff
Enter fullscreen mode Exit fullscreen mode

This should always be set. There's no downside.

Referrer-Policy

Controls how much of your URL is sent in the Referer header when users navigate away:

Referrer-Policy: strict-origin-when-cross-origin
Enter fullscreen mode Exit fullscreen mode

Options from most to least permissive:

  • no-referrer-when-downgrade (browser default) — sends full URL unless going HTTP→HTTPS
  • strict-origin-when-cross-origin — sends only origin for cross-origin, full URL for same-origin
  • same-origin — sends full URL only for same-origin requests
  • no-referrer — never sends referer

Use strict-origin-when-cross-origin for most apps. It prevents leaking paths/query strings (which may contain tokens or IDs) to third-party domains.

Permissions-Policy

Restricts which browser features your page can use — prevents malicious scripts from enabling the camera without your intent:

Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
Enter fullscreen mode Exit fullscreen mode

This disables camera, microphone, geolocation, and payment APIs for all origins. Enable them selectively:

Permissions-Policy: geolocation=(self "https://maps.example.com")
Enter fullscreen mode Exit fullscreen mode

Cross-Origin Headers (COEP, COOP, CORP)

These headers enable cross-origin isolation, which is required for SharedArrayBuffer and high-resolution timers (mitigating Spectre-class attacks):

Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Resource-Policy: same-site
Enter fullscreen mode Exit fullscreen mode

Most apps don't need these unless they use SharedArrayBuffer. But if you use WebAssembly or audio worklets that need SharedArrayBuffer, these are required.

Complete Header Set

Express (Node.js)

Use the helmet package — it sets all of these sensibly:

const helmet = require('helmet');

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],  // relax if needed
      imgSrc: ["'self'", "data:", "https:"],
    }
  },
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true
  }
}));
Enter fullscreen mode Exit fullscreen mode

Nginx

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Content-Security-Policy "default-src 'self'; frame-ancestors 'none';" always;
Enter fullscreen mode Exit fullscreen mode

Apache

Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
Header always set X-Frame-Options "DENY"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "camera=(), microphone=()"
Enter fullscreen mode Exit fullscreen mode

Cloudflare / CDN

Transform rules let you add headers at the edge without touching your origin. In the Cloudflare dashboard: Rules → Transform Rules → Modify Response Header.

Testing Your Headers

securityheaders.com — grades your headers and lists what's missing.

Firefox DevTools: Network tab → select any request → Response Headers. You can see exactly which headers are present and their values.

curl:

curl -I https://yoursite.com
Enter fullscreen mode Exit fullscreen mode

Priority Order

If you're adding these incrementally:

  1. X-Content-Type-Options: nosniff — zero risk, immediate benefit
  2. X-Frame-Options: DENY or SAMEORIGIN
  3. Referrer-Policy: strict-origin-when-cross-origin
  4. Strict-Transport-Security — test with short max-age first
  5. CSP in report-only mode → then enforce
  6. Permissions-Policy — if your app doesn't use camera/mic/geo

CSP takes the most effort to get right but provides the most protection against XSS. Use report-only mode for weeks or months before switching to enforcement — you'll catch violations in the field that never showed up in testing.

Security headers are one of the few places where the protection-to-effort ratio is genuinely excellent. Add the quick ones now; work through CSP methodically.

Top comments (0)