Skip to content
SP StackPractices
intermediate By StackPractices

Complete Guide to Web Security Headers

Implement CSP, HSTS, X-Frame-Options, and secure headers. Covers content security policy, CORS, referrer policy, permissions policy, and testing with security scanners.

Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.

Complete Guide to Web Security Headers

Introduction

HTTP security headers tell the browser how to behave when handling your site’s content. They prevent clickjacking, XSS, MIME-type sniffing, downgrade attacks, and information leakage. This guide covers every major security header, how to configure them, and how to test that they work.

Content-Security-Policy (CSP)

CSP is the most powerful security header. It restricts which resources the browser is allowed to load — scripts, styles, images, fonts, frames, and connections.

Basic CSP

Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none';

CSP with CDN and inline scripts

Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.jsdelivr.net 'nonce-abc123'; style-src 'self' https://fonts.googleapis.com 'unsafe-hashes' 'sha256-abc123'; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https://api.example.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self';
<!-- Server generates a unique nonce per request -->
<script nonce="abc123">
  console.log("This inline script is allowed");
</script>
Content-Security-Policy: script-src 'self' 'nonce-abc123'

CSP with hashes (for static inline scripts)

# Generate SHA256 hash of the script content
echo -n "console.log('hello');" | openssl dgst -sha256 -binary | openssl base64 -A
Content-Security-Policy: script-src 'self' 'sha256-abc123='

Report-only mode (testing before enforcing)

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

CSP directives reference

DirectiveControls
default-srcFallback for all resource types
script-srcJavaScript sources
style-srcCSS sources
img-srcImage sources
font-srcFont sources
connect-srcXHR, fetch, WebSocket, EventSource
frame-srciframe sources
frame-ancestorsWho can embed this page (anti-clickjacking)
object-srcFlash, Java, PDF embeds
media-srcAudio and video sources
manifest-srcWeb app manifest
worker-srcWeb Workers
base-uri<base> element restriction
form-actionForm submission targets
upgrade-insecure-requestsAuto-upgrade HTTP to HTTPS

Strict-Transport-Security (HSTS)

Forces the browser to use HTTPS for all future requests to this domain.

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
  • max-age=31536000 — 1 year in seconds
  • includeSubDomains — applies to all subdomains
  • preload — opt-in to browser HSTS preload lists

HSTS preload list

Submit your domain at hstspreload.org to be included in Chrome’s bundled preload list. Requirements:

  • Valid HTTPS certificate
  • Redirect HTTP to HTTPS on the same domain
  • HSTS header with max-age >= 31536000, includeSubDomains, and preload
  • All subdomains serve HTTPS

X-Frame-Options

Prevents clickjacking by controlling who can embed your page in an iframe.

X-Frame-Options: DENY
X-Frame-Options: SAMEORIGIN

Note: frame-ancestors in CSP supersedes this header. Use CSP frame-ancestors for modern browsers, but keep X-Frame-Options for legacy support.

X-Content-Type-Options

Prevents MIME-type sniffing — the browser respects the declared Content-Type.

X-Content-Type-Options: nosniff

Referrer-Policy

Controls how much referrer information is sent with requests.

Referrer-Policy: strict-origin-when-cross-origin
ValueReferrer sent
no-referrerNone
no-referrer-when-downgradeFull URL on HTTPS→HTTPS, nothing on HTTPS→HTTP
same-originFull URL only for same-origin requests
originOrigin only (no path)
strict-originOrigin only, nothing on downgrade
origin-when-cross-originFull URL same-origin, origin only cross-origin
strict-origin-when-cross-originFull URL same-origin, origin only cross-origin, nothing on downgrade
unsafe-urlFull URL always (not recommended)

Permissions-Policy

Controls which browser features and APIs the page can use (formerly Feature-Policy).

Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(self "https://trusted.com"), usb=()

Common features

Permissions-Policy: accelerometer=(), autoplay=(), camera=(), display-capture=(), encrypted-media=(), fullscreen=(self), geolocation=(), gyroscope=(), microphone=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=()

Cross-Origin Resource Sharing (CORS)

CORS is not a single header but a set of headers that control cross-origin requests.

Simple CORS

Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

CORS with credentials

Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: Content-Type, Authorization

Note: You cannot use Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true. You must specify the exact origin.

Preflight requests

# Browser sends OPTIONS request first
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

Cross-Origin Opener Policy (COOP)

Isolates your page from other origins to prevent Spectre-style attacks.

Cross-Origin-Opener-Policy: same-origin

Cross-Origin Embedder Policy (COEP)

Controls which cross-origin resources can be loaded.

Cross-Origin-Embedder-Policy: require-corp

Cross-Origin Resource Policy (CORP)

Restricts who can embed a resource.

Cross-Origin-Resource-Policy: same-origin

Server Configuration

Nginx

server {
    listen 443 ssl http2;
    server_name example.com;

    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'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" always;
}

Apache

<IfModule mod_headers.c>
    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=(), geolocation=()"
    Header always set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
</IfModule>

Express.js

const helmet = require("helmet");

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "https://cdn.jsdelivr.net"],
      styleSrc: ["'self'", "https://fonts.googleapis.com"],
      fontSrc: ["'self'", "https://fonts.gstatic.com"],
      imgSrc: ["'self'", "data:", "https:"],
      connectSrc: ["'self'", "https://api.example.com"],
      frameAncestors: ["'none'"],
      baseUri: ["'self'"],
      formAction: ["'self'"],
    },
  },
  strictTransportSecurity: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true,
  },
}));

Testing

Online scanners

Browser DevTools

# Chrome DevTools → Security tab
# Shows: TLS connection, security headers, insecure content warnings

# Chrome DevTools → Network tab → Response Headers
# Inspect each header on any response

CSP violation reporting

// Server endpoint to receive CSP violation reports
app.post("/csp-report", express.json({ type: "application/csp-report" }), (req, res) => {
  console.log("CSP violation:", req.body);
  res.status(204).end();
});

Best Practices

  • Start with CSP report-only — identify violations before enforcing
  • Use nonces over hashes for dynamic content — hashes break when content changes
  • Set frame-ancestors 'none' — strongest clickjacking protection
  • Always include upgrade-insecure-requests — auto-upgrade HTTP resources
  • Use strict-origin-when-cross-origin referrer policy — good privacy default
  • Preload HSTS — protect against first-visit downgrade attacks
  • Test with securityheaders.com — aim for A+ rating
  • Set headers on all responses — use always in Nginx/Apache to include error responses
  • Restrict Permissions-Policy aggressively — disable features you do not use
  • Use Helmet for Node.js — sensible defaults with easy customization
  • Review CSP monthly — new third-party scripts may break under strict CSP
  • Separate CORS per route — do not set global Access-Control-Allow-Origin: *

Common Mistakes

  • Using unsafe-inline in CSP — defeats XSS protection entirely
  • Setting Access-Control-Allow-Origin: * with credentials — browsers reject this
  • Not including always in Nginx add_header — error pages miss security headers
  • Using HSTS preload without testing — cannot be easily undone (takes months)
  • Setting X-Frame-Options: ALLOW-FROM — deprecated, use CSP frame-ancestors instead
  • Not testing CSP before enforcing — breaks production pages silently
  • Forgetting object-src 'none' — allows Flash/Java embeds
  • Not setting headers on API responses — APIs need security headers too
  • Using unsafe-eval in CSP — required by some frameworks but weakens security
  • Not monitoring CSP reports — violations go unnoticed in production

Frequently Asked Questions

What is the difference between CSP frame-ancestors and X-Frame-Options?

frame-ancestors in CSP is the modern replacement for X-Frame-Options. It supports multiple origins and wildcards, while X-Frame-Options only supports DENY or SAMEORIGIN. Keep both for legacy browser support, but use CSP as the primary control.

How do I debug CSP violations?

Check the browser console — CSP violations are logged with the blocked URL and the directive that blocked it. Use Content-Security-Policy-Report-Only to collect violations without breaking the page. Set up a reporting endpoint to aggregate violations in production.

Should I use strict-dynamic in CSP?

strict-dynamic allows scripts loaded by trusted scripts (nonces or hashes) to load other scripts. This reduces the need to maintain a whitelist of script URLs. It is recommended for applications with dynamic script loading, but requires CSP Level 3 support (Chrome 52+, Firefox 52+, Safari 15.4+).