HTTP Security Headers in 2026: What to Set, What to Skip, and What You're Probably Getting Wrong

Content-Security-Policy, HSTS, COOP, COEP, and Permissions-Policy explained with concrete examples. Learn what each header actually does, which common misconfigurations defeat the protection, and how to implement the right stack for your application.

HTTP security headers eliminate entire classes of browser-based attacks — cross-site scripting, clickjacking, MIME-sniffing — without changing a line of application logic. The return on effort is hard to beat. Yet fewer than 25% of the top one million websites deploy a meaningful Content-Security-Policy, and roughly 40% still lack the basic X-Content-Type-Options header.

This article covers what actually matters in 2026: which headers are essential, which misconfigurations defeat the protection, and how to implement the right stack in Node.js, Python, and your reverse proxy.

The Headers That Actually Matter

Content-Security-Policy (CSP)

CSP is the most powerful browser security header available and the one most often misconfigured. It instructs the browser on where scripts, styles, images, and other resources can be loaded from — and whether inline scripts are allowed.

The common mistake: unsafe-inline

Content-Security-Policy: script-src 'self' 'unsafe-inline'

This defeats the entire purpose of CSP. unsafe-inline allows execution of any inline script, including attacker-injected ones. You’ve told the browser “trust anything in a <script> tag,” which is exactly what XSS exploits.

Modern approach: nonce-based CSP with strict-dynamic

For server-rendered applications, generate a unique random nonce for each response and use it with strict-dynamic:

// Express middleware
const crypto = require('crypto');

app.use((req, res, next) => {
  const nonce = crypto.randomBytes(16).toString('base64');
  res.locals.nonce = nonce;
  res.setHeader(
    'Content-Security-Policy',
    `script-src 'nonce-${nonce}' 'strict-dynamic'; ` +
    `style-src 'nonce-${nonce}' https:; ` +
    `object-src 'none'; ` +
    `base-uri 'none'; ` +
    `frame-ancestors 'none';`
  );
  next();
});

In your templates, add the nonce to every <script> tag:

<script nonce="<%= nonce %>">
  // This script is trusted
</script>

strict-dynamic means scripts that your nonce-trusted scripts create dynamically are also trusted — which makes CSP compatible with modern JavaScript frameworks that add scripts at runtime without needing to enumerate every CDN domain.

For static HTML: hash-based CSP

Content-Security-Policy: script-src 'sha256-{hash_of_inline_script}' 'strict-dynamic'; object-src 'none'; base-uri 'none';

Calculate the SHA-256 hash of your inline script’s exact content. Every inline script needs its own hash. This is the right approach when you can’t generate nonces server-side.

Start in report-only mode

If you’re adding CSP to an existing application, deploy Content-Security-Policy-Report-Only first:

Content-Security-Policy-Report-Only: script-src 'nonce-{nonce}' 'strict-dynamic'; object-src 'none'; report-uri /csp-violations

You’ll see violations in your logs without breaking anything. Fix violations, then switch to enforcement.

Strict-Transport-Security (HSTS)

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

HSTS tells browsers to always use HTTPS for your domain, preventing SSL stripping attacks. Once set, the browser will refuse HTTP connections for the duration of the max-age. includeSubDomains extends protection to all subdomains. preload opts you into browser-maintained preload lists — meaning even first-time visitors get HTTPS enforcement.

Start with a short max-age (e.g., 300 seconds) and extend it once you’re confident all your subdomains serve HTTPS.

X-Content-Type-Options

X-Content-Type-Options: nosniff

Prevents MIME-type sniffing — browsers guessing at a resource’s type based on content rather than the declared Content-Type. Without this header, a browser might interpret an image upload that contains JavaScript as a script. One line. Set it on every response.

X-Frame-Options vs CSP frame-ancestors

The legacy approach:

X-Frame-Options: DENY

The modern approach (use this):

Content-Security-Policy: frame-ancestors 'none';

CSP frame-ancestors supersedes X-Frame-Options in modern browsers, offers finer-grained control (you can whitelist specific parent origins), and is part of your existing CSP header. If you’re setting CSP already, use frame-ancestors rather than a separate X-Frame-Options header. Keep X-Frame-Options for legacy browser compatibility.

Permissions-Policy

Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()

Disables powerful browser features by default. If your application doesn’t use the camera, microphone, geolocation, or payment APIs, explicitly deny them. An attacker who achieves XSS can’t then access the user’s camera if the feature is denied at the policy level.

Cross-Origin-Opener-Policy (COOP)

Cross-Origin-Opener-Policy: same-origin-allow-popups

Protects against tabnapping attacks, where a page opened via window.open() can manipulate the opener’s location. same-origin-allow-popups is the right value for most applications — it prevents cross-origin pages from accessing your window while preserving communication with windows your page opens. The simpler same-origin breaks OAuth popups and payment flows.

Cross-Origin-Embedder-Policy (COEP)

Cross-Origin-Embedder-Policy: require-corp

Required if you need SharedArrayBuffer or high-resolution performance timers — features the browser restricts unless the page is “cross-origin isolated.” COEP requires all cross-origin resources to either include CORS headers or a Cross-Origin-Resource-Policy header. This is disruptive to enable on existing applications; only set it if you specifically need cross-origin isolation.

Referrer-Policy

Referrer-Policy: strict-no-referrer

Prevents sensitive URL fragments (auth tokens, reset codes) from leaking via the Referer header to third-party resources. strict-no-referrer sends no referrer information at all. If you need analytics to see internal referrers, use strict-origin-when-cross-origin instead.

The Minimal Header Stack

For most web applications, this is the right starting configuration:

Content-Security-Policy: script-src 'nonce-{nonce}' 'strict-dynamic'; style-src 'nonce-{nonce}' https:; object-src 'none'; base-uri 'none'; frame-ancestors 'none';
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
Cross-Origin-Opener-Policy: same-origin-allow-popups
Referrer-Policy: strict-no-referrer

Python (Flask) implementation

from flask import Flask, g
import secrets, base64

app = Flask(__name__)

@app.after_request
def set_security_headers(response):
    nonce = base64.b64encode(secrets.token_bytes(16)).decode()
    g.csp_nonce = nonce
    response.headers['Content-Security-Policy'] = (
        f"script-src 'nonce-{nonce}' 'strict-dynamic'; "
        "style-src https: 'unsafe-inline'; "
        "object-src 'none'; base-uri 'none'; frame-ancestors 'none';"
    )
    response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains; preload'
    response.headers['X-Content-Type-Options'] = 'nosniff'
    response.headers['X-Frame-Options'] = 'DENY'
    response.headers['Permissions-Policy'] = 'camera=(), microphone=(), geolocation=()'
    response.headers['Cross-Origin-Opener-Policy'] = 'same-origin-allow-popups'
    response.headers['Referrer-Policy'] = 'strict-no-referrer'
    return response

Nginx configuration

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Cross-Origin-Opener-Policy "same-origin-allow-popups" always;
add_header Referrer-Policy "strict-no-referrer" always;
# Note: Set CSP in application layer for nonce support, not here

Testing your headers

Use securityheaders.com to scan your deployed application. Google’s CSP Evaluator at csp-evaluator.withgoogle.com checks for CSP-specific weaknesses. Both are free and show exactly what’s missing or misconfigured. Aim for a clean scan before shipping any user-facing application.