HTTP security headers: An easy way to harden your web applications

HTTP security headers: An easy way to harden your web applications

Modern browsers and servers give you powerful security controls through HTTP headers. Set them once, and the browser enforces policy on every request. You block whole classes of attacks before application code runs. That includes clickjacking, cross-site scripting, MIME sniffing, insecure transport, cross-origin data leakage, and weak session handling. Headers complement input validation, output encoding, and authentication. They align with OWASP Top 10 risks and reduce misconfiguration drift across stacks.

What these headers do for you

  • Enforce HTTPS and stop downgrade attacks with HSTS.
  • Constrain where scripts, styles, and other resources can load with Content-Security-Policy (CSP).
  • Block clickjacking attacks by controlling where your site can be framed using the frame-ancestors directive (which is part of CSP and supersedes the older X-Frame-Options header).
  • Disable MIME sniffing with X-Content-Type-Options.
  • Limit referrer data exposure with Referrer-Policy.
  • Gate powerful browser features with Permissions-Policy.
  • Achieve cross-origin isolation with COOP, COEP, and CORP when the feature set requires it.
  • Control legitimate cross-origin API access with CORS and verify intent with Fetch Metadata.
  • Protect sessions with Secure, HttpOnly, and SameSite cookies.
  • Verify CDN assets with Subresource Integrity.

How to work with them

  • Treat headers as configuration as code. Keep them in version control.
  • Roll out with report-only and canary routes, then enforce.
  • Validate in CI with curl checks and trusted scanners.
  • Use DAST to confirm real-world behavior in staging and production.
  • Revisit policies when you add new third-party scripts, new APIs, or new features.

TLDR

  • Start with CSP in Report-Only. Use default-src 'self', nonces for inline scripts, and frame-ancestors 'self'. Keep reporting on.
  • Turn on HTTP Strict Transport Security after fixing HTTPS. Use max-age=31536000; includeSubDomains. Add preload only when you are ready for every subdomain.
  • Stop using HPKP, X-XSS-Protection, and Expect-CT. Use HSTS and CSP instead.
  • Set Referrer-Policy: strict-origin-when-cross-origin. Use no-referrer if you handle sensitive data.
  • Enable cross-origin isolation only when needed. Use COOP: same-origin and COEP: require-corp or credentialless. Add CORP on your assets.
  • Always send X-Content-Type-Options: nosniff.
  • Lock cookies with Secure; HttpOnly; SameSite=Lax or Strict. Use SameSite=None; Secure only for third-party cookies.
  • Use CORS allowlists. Never combine * with credentials. Cache preflight with a short Access-Control-Max-Age.
  • Add Fetch Metadata checks on state-changing endpoints. Reject cross-site unsafe methods fast.
  • Use Subresource Integrity for CDN scripts and styles.

Major headers, secure defaults, and examples

Content-Security-Policy (CSP)

Purpose: restrict where your page can load scripts, styles, images, frames, and more. This mitigates XSS and data injection.

Safe starting point

Content-Security-Policy: default-src 'self'; base-uri 'self'; object-src 'none';
script-src 'self' 'nonce-{RANDOM}';
style-src 'self';
img-src 'self' data:;
frame-ancestors 'self';
upgrade-insecure-requests

Report-Only while tuning

Reporting-Endpoints: csp="https://example.com/csp-reports"
Content-Security-Policy-Report-Only: default-src 'self'; base-uri 'self'; object-src 'none'; script-src 'self' 'nonce-{RANDOM}'; style-src 'self'; img-src 'self' data:; frame-ancestors 'self'; upgrade-insecure-requests; report-to csp; report-uri https://example.com/csp-legacy

Reporting API Setup

Report-To: {"group":"csp-endpoint","max_age":10886400,"endpoints":[{"url":"https://example.com/csp-reports"}],"include_subdomains":true}

Note: Use report-to (modern Reporting API) as primary. Keep report-uri only for legacy browser support during transition.

Tips

  • Prefer nonces for inline bootstraps. Generate a random nonce per response.
  • Replace X-Frame-Options with frame-ancestors.
  • Keep report-uri for legacy browsers.
  • Use report-to for the modern Reporting API. The value csp-endpoint must correspond to a group name defined in a separate Report-To HTTP header.

Deployment checklist

  • Start with Content-Security-Policy-Report-Only on all pages.
  • Generate a new cryptographically random nonce per response. Do not reuse across requests.
  • Remove inline event handlers and eval. Replace with script files that use the page nonce.
  • List third-party script hosts explicitly. Avoid wildcards and data: for scripts.
  • Add frame-ancestors 'self' even if you think framing is not used.
  • Keep object-src 'none'. Plugins should not be needed in 2025.
  • Switch to enforcing CSP when violations stabilize, then keep reporting enabled.

Third-party scripts strategy

  • Prefer server-side or tag-manager hosted first-party mirrors when possible.
  • For analytics, allow only the exact origins and required directives. Block connect-src to unknown hosts.
  • Rotate nonces and do not allow 'unsafe-inline' or 'unsafe-eval'.

CSP comparison

NamePurposeCritical directivesExample valueBrowser supportBreakage riskTooling notes
Content-Security-PolicyClient-side content isolation policy that constrains resource fetch and execution contexts to declared origins, mitigating reflected, stored, and DOM-based XSS, injection of active content, and UI redress via frame-ancestorsdefault-src, script-src with nonce, frame-ancestors, object-src 'none', upgrade-insecure-requestsSee policy aboveChrome 25+, Edge 12+, Firefox 23+, Safari 7+, Opera 15+, iOS Safari 6+, IE 10-11 partial via X-Content-Security-PolicyMedium to high. Inline scripts without nonce or hash are blocked, undeclared third-party hosts fail to load, eval-like constructs break, and legacy tag managers or inlined event handlers stop workingUse Reporting API and keep report-uri while migrating

HTTP Strict-Transport-Security (HSTS)

Purpose: force HTTPS. Block HTTP and invalid TLS bypass attempts.

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

Preload

  • Only add preload when every subdomain uses HTTPS and will keep it. Once you are confident, you can submit your domain to the official preload list at hstspreload.org (opens in new window).
  • Rolling back is slow and risky.

Warning: HSTS preload is permanent for practical purposes. Removal from browser preload lists can take 6-12 months and requires coordination with all major browsers. Only submit to hstspreload.org (opens in new window) after:

  • All subdomains have valid HTTPS (including internal tools)
  • No HTTP fallback exists anywhere
  • You have documented business approval for permanent HTTPS enforcement

Preload readiness checklist

  • Every subdomain serves HTTPS with valid certificates.
  • Permanent 301 from HTTP to HTTPS is in place for all hosts.
  • No internal or legacy systems require plain HTTP.
  • Set max-age to at least 31536000 and include includeSubDomains for two weeks before submitting.
  • Document a rollback plan. Preload removal takes time and cannot be done instantly.

HSTS comparison

NamePurposeCritical directivesExample valueBrowser supportBreakage riskTooling notes
HSTSTransport security policy that instructs user agents to auto-upgrade HTTP to HTTPS and pin HTTPS-only for the origin, preventing protocol downgrade and SSL strippingmax-age, includeSubDomains, optional preloadmax-age=31536000; includeSubDomainsChrome 4+, Edge 12+, Firefox 4+, Safari 7+, Opera 12+, IE 11 on Windows 8.1 or Windows 7 with KB3058515High if any subdomain or legacy endpoint still serves HTTP or presents invalid TLS, as the browser will hard-fail those navigationsTest redirects and certs first

Referrer-Policy

Purpose: control how much referrer info you send.

Default

Referrer-Policy: strict-origin-when-cross-origin

Use no-referrer for maximum privacy.

Referrer-Policy comparison

NamePurposeCritical directivesExample valueBrowser supportBreakage riskTooling notes
Referrer-PolicyRequest privacy control that bounds the Referer header granularity across same-origin and cross-origin navigations and subresource fetches, reducing origin and path leakageOne token like no-referrer, strict-origin-when-cross-originstrict-origin-when-cross-originChrome 61+, Edge 79+, Firefox 52+, Safari 11.1+, Opera 48+, iOS Safari 12+Low. Some analytics, A/B routing, or payment callbacks relying on full-path referrers may see reduced visibilityCheck analytics that rely on full referrers

Permissions-Policy

Purpose: control access to features like geolocation, camera, and microphone.

Tight default

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

Audit steps

  • Search your code for feature APIs.
  • Use DevTools to see prompts and blocked features.
  • Open policies for routes that need them.

Common features to gate

  • geolocation=(), camera=(), microphone=() for pages that never use them.
  • payment=() and usb=() for public marketing pages.
  • fullscreen=(self) for apps that use media viewers.

Permissions-Policy comparison

NamePurposeCritical directivesExample valueBrowser supportBreakage riskTooling notes
Permissions-PolicyFeature gating policy that allows or denies powerful browser capabilities per origin and embedding context, enforcing least privilege for APIs like geolocation, camera, and microphonePer-feature allowlistsgeolocation=(), camera=(), microphone=()Chrome 88+, Edge 88+, Opera 74+, Firefox not supported, Safari not supportedLow to medium. Pages or iframes that implicitly relied on prompts will see capability denials until explicitly allowedSet per route if needed

X-Content-Type-Options

Purpose: stop MIME sniffing. This prevents the browser from guessing the content type, which can be dangerous (e.g., it might execute a text file uploaded by a user if it looks like a script).

Value

X-Content-Type-Options: nosniff

X-Content-Type-Options comparison

NamePurposeCritical directivesExample valueBrowser supportBreakage riskTooling notes
X-Content-Type-OptionsMIME enforcement directive that disables content sniffing for script and style responses, requiring a correct Content-Type to executenosniffnosniffChrome 64+, Edge 12+, Firefox 50+, Safari 11+, Opera 51+, IE 8+Low, provided all assets are served with accurate MIME types. Mis-typed resources will be blockedServe correct Content-Type

COOP, COEP, CORP

Purpose: isolate your app from other origins and block cross-site leaks. Required for some advanced features like SharedArrayBuffer.

Clear-Site-Data: "cache", "cookies", "storage"  # Use on logout endpoints
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Resource-Policy: same-origin

Notes

  • COEP: credentialless can simplify loading third-party resources without credentials. Check support first.
  • Audit embeds and postMessage use before rollout.

When to use cross-origin isolation

  • You need SharedArrayBuffer or performance APIs that require isolation.
  • You want stronger protections against cross-site leaks and popup communication.

Troubleshooting

  • Images, fonts, or WASM failing to load usually lack CORP. Add Cross-Origin-Resource-Policy: same-origin on your assets or serve them from the same origin.
  • Third-party widgets may not embed under COEP. Host them first-party or use credentialless if supported.

COOP, COEP, CORP comparison

NamePurposeCritical directivesExample valueBrowser supportBreakage riskTooling notes
COOPBrowsing context group isolation that places the page in a separate group from cross-origin documents, mitigating cross-window data leaks and certain XS-Leakssame-originsame-originChrome 83+, Edge 83+, Firefox 79+, Safari 15.2+, Opera 69+Medium. Cross-origin window references, opener relationships, and postMessage flows that assume same group can breakAudit postMessage and window.open
COEPEmbedding policy that requires cross-origin resources to explicitly grant embedding via CORP or CORS, enabling cross-origin isolation and blocking opaque no-cors embedsrequire-corp or credentiallessrequire-corpChrome 83+, Edge 83+, Opera 69+, Firefox not supported, Safari not supportedMedium to high. Third-party assets without CORP or CORS headers will fail to load, including fonts, images, and WASMAdd CORP on your assets
CORPResource protection policy that declares which origins may embed a resource in no-cors contexts, preventing unauthorized cross-origin consumptionsame-origin, same-site, cross-originsame-originChrome 73+, Edge 79+, Firefox 74+, Safari not supported, iOS Safari not supportedLow to medium. Cross-site consumers without an allowed relationship will fail to load protected assetsSet on static assets

Cross-Origin Resource Sharing (CORS)

Purpose: allow specific cross-origin requests.

Safe example

# Preflight (OPTIONS)
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 600
Vary: Origin, Access-Control-Request-Method, Access-Control-Request-Headers

# Simple/actual response
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Credentials: true
Vary: Origin

Access-Control-Expose-Headers: Content-Length, X-Request-ID
Cross-Origin-Resource-Policy: same-origin

Preflight

  • Browsers send OPTIONS to check policy.
  • Cache with a short Access-Control-Max-Age.

Credentialed requests rules

  • Do not use * with credentials. Echo the allowed origin from an allowlist.
  • Always set Vary: Origin when responses differ by origin.
  • Allow only necessary methods and headers. Avoid broad Access-Control-Allow-Headers: *.

Preflight caching gotchas

  • Keep Access-Control-Max-Age modest in production, such as 600 seconds, so policy changes take effect quickly.
  • CDNs may cache OPTIONS. Bypass or scope caching for preflight paths.

CORS comparison

NamePurposeCritical directivesExample valueBrowser supportBreakage riskTooling notes
CORS headersCross-origin request authorization framework that governs which origins, methods, and headers may access protected resources via the fetch/XHR pipeline, including preflight negotiationAccess-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Allow-Credentials, Access-Control-Max-AgeSee exampleChrome 4+, Edge 12+, Firefox 3.5+, Safari 4+, Opera 12.1+, IE 10+Medium. Incorrect allowlists, missing Vary, or mis-scoped allowed headers block legitimate clients or enable cached policy mismatchesAlways set Vary: Origin

Fetch Metadata

Purpose: identify the request context and block cross-site abuse. Use it to harden CSRF defenses.

Server check idea

# Pseudocode: reject unsafe, non-navigational cross-site requests
site = get_header("Sec-Fetch-Site")
mode = get_header("Sec-Fetch-Mode")
is_unsafe_method = (method in [POST, PUT, PATCH, DELETE])
is_cross_site = (site == "cross-site")
is_navigational = (mode == "navigate")

# Block requests that are cross-site, unsafe, AND not a top-level navigation
if (is_cross_site && is_unsafe_method && !is_navigational) {
  reject()
}

Express middleware example

function fetchMetadataGuard(req, res, next) {
  const site = req.get("Sec-Fetch-Site") || "";
  const mode = req.get("Sec-Fetch-Mode") || "";
  const dest = req.get("Sec-Fetch-Dest") || "";
  const unsafe = ["POST", "PUT", "PATCH", "DELETE"].includes(req.method);
  const allowedSameSite = site === "" || site === "same-origin" || site === "same-site";
  const isNavigation = mode === "navigate" && dest === "document";

  if (unsafe && !allowedSameSite && !isNavigation) return res.sendStatus(403);
  next();
}

Fetch Metadata comparison

NamePurposeCritical directivesExample valueBrowser supportBreakage riskTooling notes
Fetch MetadataRequest context validation that inspects Sec-Fetch-* headers to differentiate same-origin, same-site, and cross-site requests, enabling server-side CSRF and XS-Leak hardeningSec-Fetch-Site, Sec-Fetch-Mode, Sec-Fetch-Dest, Sec-Fetch-UserReject unsafe methods when cross-siteChrome 76+, Edge 79+, Opera 63+, Firefox not supported, Safari not supportedLow when scoped to unsafe methods. Overly strict rules can block legitimate cross-site navigations or OAuth redirectsLog rejects to tune

Subresource Integrity (SRI)

Purpose: ensure CDN resources are not tampered with.

Example

<script src="https://cdn.example.com/app.js" integrity="sha384-BASE64HASH" crossorigin="anonymous"></script>

Notes

  • Pin versions or update hashes automatically.
  • Use for scripts and styles.

Generate and Verify Hashes

  • Generate: openssl dgst -sha384 -binary app.js | openssl base64 -A
  • Verify: Always test SRI in staging before production. Broken hashes = broken site.
  • Automate: Use build tools to generate hashes during deployment

SRI comparison

NamePurposeCritical directivesExample valueBrowser supportBreakage riskTooling notes
SRISubresource integrity verification that binds external scripts and styles to cryptographic digests, preventing execution of tampered CDN assetsintegrity, crossoriginSee exampleChrome 45+, Edge 79+, Firefox 43+, Safari 11+, Opera 32+, iOS Safari 11.3+Medium. Any upstream asset change without a matching hash update causes hard failuresAutomate hash updates

Secure cookies

Purpose: protect sessions.

Safe example

Set-Cookie: session=abc...; Path=/; Secure; HttpOnly; SameSite=Lax

Notes

  • Modern browsers default to Lax when SameSite is missing. Be explicit.
  • Use SameSite=None; Secure for third-party cookies only.
  • Set Path=/ only when needed. Narrower paths reduce exposure.
  • Scope Domain to the fewest hosts possible. Avoid super-domain cookies on large estates.
  • Rotate session IDs on login and privilege changes. Invalidate on logout.
  • Use short expirations for session cookies. Prefer server-side session stores with revocation.

Cookies comparison

NamePurposeCritical directivesExample valueBrowser supportBreakage riskTooling notes
Set-CookieSession confidentiality and CSRF defense-in-depth via Secure transport, script-inaccessibility with HttpOnly, and cross-site semantics with SameSiteSecure, HttpOnly, SameSiteSecure; HttpOnly; SameSite=LaxHttpOnly and Secure supported by all modern browsers. SameSite=None requires Chrome 67+, Edge 16+, Firefox 60+, Safari 13+, iOS Safari 13+Low to medium. Cross-site flows, embedded SSO, or legacy third-party integrations may fail without SameSite=None; Secure where requiredAudit cross-site flows before tightening

Obsolete or discouraged headers

HeaderRemoved/Deprecated/etc. since whenReasonReplacement
HTTP Public-Key-Pins (HPKP)Removed, 2018-05-29, Risk of denial-of-service and hostile pinningHigh risk of self-inflicted lockoutReplace Public-Key-Pins with HSTS preload and Certificate Transparency
X-XSS-ProtectionDeprecated, 2019-10, Deprecated by browsers, can create XSS vulnerabilities and conflicts with CSPIneffective and misleadingReplace X-XSS-Protection with Content-Security-Policy with no inline scripts
Expect-CTDeprecated, 2022-11, CT enforced by Chromium, header obsoleteCT now built into ecosystemReplace Expect-CT with rely on Certificate Transparency and remove header
X-Frame-OptionsSuperseded by modern control, 2016-12-15 reference for CSP frame-ancestors adoption, Limited directives and interoperabilityLimited syntax and controlReplace X-Frame-Options with Content-Security-Policy: frame-ancestors

Configuration recipes

Nginx

# /etc/nginx/conf.d/security.conf
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Resource-Policy "same-origin" always;

location / {
  set_by_lua_block $csp_nonce {
    local rand = require("resty.random").bytes(16, true)
    return ngx.encode_base64(rand)
  }

  add_header Content-Security-Policy
    "default-src 'self'; base-uri 'self'; object-src 'none'; \
    script-src 'self' 'nonce-$csp_nonce'; style-src 'self'; img-src 'self' data:; \
    frame-ancestors 'self'; upgrade-insecure-requests" always;
}

# CORS example for API
location /api/ {
    if ($request_method = 'OPTIONS') {
        add_header Access-Control-Allow-Origin "https://example.com";
        add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
        add_header Access-Control-Allow-Headers "Content-Type";
        add_header Access-Control-Allow-Credentials "true";
        add_header Access-Control-Max-Age 600;
        return 204;
    }
    add_header Access-Control-Allow-Origin "https://example.com";
    add_header Access-Control-Allow-Credentials "true";
    add_header Vary "Origin";
    proxy_pass http://backend;
}

Apache httpd

# In <VirtualHost> or .htaccess with AllowOverride All
# e.g., SetEnvIfNoCase Request_URI ".*" CSP_NONCE=%{UNIQUE_ID}e
Header always set Content-Security-Policy "default-src 'self'; base-uri 'self'; object-src 'none'; script-src 'self' 'nonce-%{CSP_NONCE}e'; style-src 'self'; img-src 'self' data:; frame-ancestors 'self'; upgrade-insecure-requests"
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "geolocation=(), camera=(), microphone=()"
Header always set X-Content-Type-Options "nosniff"
Header always set Cross-Origin-Opener-Policy "same-origin"
Header always set Cross-Origin-Embedder-Policy "require-corp"
Header always set Cross-Origin-Resource-Policy "same-origin"

# CORS example
<Location /api/>
  Header always set Access-Control-Allow-Origin "https://example.com"
  Header always set Access-Control-Allow-Credentials "true"
  Header always set Vary "Origin"
  RewriteEngine On
  RewriteCond %{REQUEST_METHOD} OPTIONS
  RewriteRule ^ - [R=204,L]
  Header always set Access-Control-Allow-Methods "GET, POST, OPTIONS"
  Header always set Access-Control-Allow-Headers "Content-Type"
  Header always set Access-Control-Max-Age "600"
</Location>

Node.js Express

import express from "express";
import crypto from "crypto";

const app = express();

// CSP nonce per response
app.use((req, res, next) => {
  res.locals.cspNonce = crypto.randomBytes(16).toString("base64");
  next();
});

app.use((req, res, next) => {
  const nonce = res.locals.cspNonce;
  res.setHeader("Content-Security-Policy",
    "default-src 'self'; base-uri 'self'; object-src 'none'; " +
    "script-src 'self' 'nonce-" + nonce + "'; style-src 'self'; img-src 'self' data:; " +
    "frame-ancestors 'self'; upgrade-insecure-requests");
  res.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
  res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
  res.setHeader("Permissions-Policy", "geolocation=(), camera=(), microphone=()");
  res.setHeader("X-Content-Type-Options", "nosniff");
  res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
  res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
  res.setHeader("Cross-Origin-Resource-Policy", "same-origin");
  next();
});

// Fetch Metadata guard
app.use((req, res, next) => {
  const site = req.get("Sec-Fetch-Site");
  const unsafe = ["POST","PUT","PATCH","DELETE"].includes(req.method);
  if (site === "cross-site" && unsafe) return res.status(403).end();
  next();
});

// CORS allowlist example
app.use("/api", (req, res, next) => {
  const origin = req.get("Origin");
  if (origin === "https://example.com") {
    res.setHeader("Access-Control-Allow-Origin", origin);
    res.setHeader("Access-Control-Allow-Credentials", "true");
    res.setHeader("Vary", "Origin");
  }
  if (req.method === "OPTIONS") {
    res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
    res.setHeader("Access-Control-Allow-Headers", "Content-Type");
    res.setHeader("Access-Control-Max-Age", "600");
    return res.status(204).end();
  }
  next();
});

app.get("/", (req, res) => {
  res.send(`<script nonce="${res.locals.cspNonce}">console.log("ok")</script>`);
});

app.listen(3000);

Cloudflare Workers

export default {
  async fetch(req, env, ctx) {
    const headers = new Headers({
      "Content-Security-Policy":
        "default-src 'self'; base-uri 'self'; object-src 'none'; " +
        "script-src 'self'; style-src 'self'; img-src 'self' data:; " +
        "frame-ancestors 'self'; upgrade-insecure-requests",
      "Strict-Transport-Security": "max-age=31536000; includeSubDomains",
      "Referrer-Policy": "strict-origin-when-cross-origin",
      "Permissions-Policy": "geolocation=(), camera=(), microphone=()",
      "X-Content-Type-Options": "nosniff",
      "Cross-Origin-Opener-Policy": "same-origin",
      "Cross-Origin-Embedder-Policy": "require-corp",
      "Cross-Origin-Resource-Policy": "same-origin"
    });

    if (req.method === "OPTIONS") {
      headers.set("Access-Control-Allow-Origin", "https://example.com");
      headers.set("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
      headers.set("Access-Control-Allow-Headers", "Content-Type");
      headers.set("Access-Control-Allow-Credentials", "true");
      headers.set("Access-Control-Max-Age", "600");
      return new Response(null, { status: 204, headers });
    }

    headers.set("Access-Control-Allow-Origin", "https://example.com");
    headers.set("Access-Control-Allow-Credentials", "true");
    headers.append("Vary", "Origin");

    return new Response("OK", { headers });
  }
};

Note on Nonces in Edge Workers

This example provides a static CSP for simplicity. Implementing nonces (as recommended) in an edge worker (like Cloudflare) is more complex. It requires you to:

  • Generate a random nonce (e.g., using crypto.randomUUID()).
  • Fetch the origin response.
  • Use an HTML rewriter (like HTMLRewriter in Cloudflare) to stream the response, inject the nonce attribute into your <script> and <style> tags, and then
  • Add the Content-Security-Policy header to the final response, including the generated 'nonce-...' value.

Verification

# Show all response headers
curl -s -D - https://example.com/ -o /dev/null

# Check a single header
curl -s -D - https://example.com/ -o /dev/null | grep -i strict-transport-security

# Check CORS preflight
curl -s -D - -X OPTIONS https://api.example.com/endpoint \
  -H "Origin: https://example.com" \
  -H "Access-Control-Request-Method: POST" -o /dev/null

Security Header Validation Script

#!/bin/bash
# Comprehensive security header check
URL="https://example.com"
echo "Testing security headers for $URL"

CRITICAL_HEADERS=(
  "Content-Security-Policy"
  "Strict-Transport-Security"
  "X-Content-Type-Options"
  "Referrer-Policy"
  "Permissions-Policy"
)

# Fail if any required header is missing
for h in "${CRITICAL_HEADERS[@]}"; do
  curl -s -D - https://example.com/ -o /dev/null | grep -iq "^$h:" || { echo "Missing $h"; exit 1; }
done

# Ensure CSP contains frame-ancestors
CSP_VALUE=$(curl -s -D - https://example.com/ -o /dev/null | awk 'BEGIN{IGNORECASE=1}/^Content-Security-Policy:/{print substr($0,index($0,$2))}')
echo "$CSP_VALUE" | grep -iq "frame-ancestors" || { echo "CSP missing frame-ancestors"; exit 1; }

# Test HSTS preload readiness
echo "HSTS Preload Check:"
curl -s "https://hstspreload.org/api/v2/status?domain=$(echo $URL | sed 's|https://||')" | jq .

Expected outcomes

  • Document request shows CSP, HSTS, Referrer-Policy, COOP, COEP, CORP, and X-Content-Type-Options.
  • Preflight shows the CORS headers you configured.
  • grep finds the exact header and value.

Browser DevTools

  • Open Network tab, load a page, select the document request, and inspect response headers.
  • Keep Console open to capture CSP violations while in Report-Only.
  • Check the Application panel for cookies and their flags.

Quick steps

  • Chrome: Network tab then select the document then Response Headers. Filter with csp,strict-transport, referrer-policy.
  • Open the Console and reload to see Content Security Policy violations while in Report-Only.
  • Firefox: Storage panel then Cookies. Check Secure, HttpOnly, and SameSite flags.

Rollout and monitoring

  • Inventory & Baseline (Week 1)
    • Map all domains/subdomains
    • Document current header state
    • Identify all third-party dependencies
  • CSP Report-Only (Weeks 2-3)
    • Deploy CSP in report-only mode
    • Monitor violations for 14+ days
    • Fix legitimate violations in code
  • Basic Headers (Week 4)
    • Deploy `X-Content-Type-Options: nosniff`
    • Deploy `Referrer-Policy: strict-origin-when-cross-origin`
    • Deploy `Permissions-Policy` with safe defaults
  • HSTS Gradual (Weeks 5-8)
    • Start with `max-age=300` on non-critical subdomain
    • Gradually increase to `max-age=31536000`
    • Add `includeSubDomains` only after all subdomains verified
  • CSP Enforcement (Week 9+)
    • Switch CSP to enforce mode
    • Keep reporting enabled
    • Monitor for new violations
  • Advanced Isolation (Optional)
    • Deploy COOP/COEP/CORP only if needed
    • Test cross-origin isolation requirements

Common mistakes

  • Using Access-Control-Allow-Origin: * with credentials. This is invalid and unsafe. Use an allowlist and Vary: Origin.
  • Sending only X-Frame-Options. Use frame-ancestors in CSP.
  • Forgetting X-Content-Type-Options: nosniff.
  • Not setting cookie flags. Use Secure; HttpOnly; SameSite.
  • Preloading HSTS before you are ready.
  • Breaking third-party widgets with COEP or CORP. Audit and add explicit permissions.
  • Treating CSP as a silver bullet. You still need output encoding and input validation.

More pitfalls and quick fixes

  • CSP allows 'unsafe-inline'. Fix by using nonces or hashes and removing inline handlers.
  • Missing frame-ancestors in CSP. Add it even if you also send X-Frame-Options.
  • COEP require-corp deployed without CORP on static assets. Add Cross-Origin-Resource-Policy on images, fonts, and WASM.
  • CORS Access-Control-Allow-Headers set to *. Replace with exact headers used.
  • Cookies with SameSite=None but missing Secure. Always pair None with Secure.

Questions

Prefer frame-ancestors. Some teams keep both for legacy, but CSP is the modern control.

Allow only needed origins. Use nonces for your inline bootstraps. Avoid wildcards.

No. Use it only when every subdomain is HTTPS and will stay HTTPS.

strict-origin-when-cross-origin fits most sites. Use no-referrer for stricter privacy.

Use both. Tokens remain primary. Fetch Metadata gives a fast rejection path.

It exposes data widely. Never combine with credentials. Use allowlists.

Only if you need cross-origin isolation or stronger cross-site leak defenses. Test embeds first.

No. It is obsolete.

Most browsers treat cookies without SameSite as Lax. Set attributes explicitly.

Use curl with OPTIONS, send Origin and Access-Control-Request-Method.

Yes. Pin versions and use SRI. This covers tampering on the CDN.

Yes. Edge is a good place. Keep app-level overrides for routes that need different policies.

Conclusion

HTTP headers give you fast, measurable gains with low risk. Ship the safe defaults first, then add stricter controls as you learn how your app behaves. Keep policies in code, test them in CI, and review them on a schedule.

Resources

Zsolt Oroszlány

Article author Zsolt Oroszlány

CEO of the creative agency Playful Sparkle, brings over 20 years of expertise in graphic design and programming. He leads innovative projects and spends his free time working out, watching movies, and experimenting with new CSS features. Zsolt's dedication to his work and hobbies drives his success in the creative industry.

Let’s amplify your success together!

Request a Free Quote

Related articles

Read the article 'Step-by-Step Guide to Building Your First Python Package'

Step-by-Step Guide to Building Your First Python Package

Creating a Python module requires more than just writing code. Modern development demands proper tooling, standardized workflows, and automation. In this guide, we’ll walk through setting up a robust Python project, complete with testing, linting, CI/CD pipelines, and PyPI publishing. Read moreabout Step-by-Step Guide to Building Your First Python Package

Read the article 'Mastering String Formatting in JavaScript with sprintf()'

Mastering String Formatting in JavaScript with sprintf()

String formatting is a fundamental aspect of programming, allowing developers to create dynamic and readable text output by embedding variables and applying specific formatting rules. Read moreabout Mastering String Formatting in JavaScript with sprintf()

Read the article 'What is Google Consent Mode V2? How to Implement It?'

What is Google Consent Mode V2? How to Implement It?

As privacy regulations like the General Data Protection Regulation (GDPR) and the California Consumer Privacy Act (CCPA) have fundamentally shifted how businesses approach user data, Google Consent Mode V2 emerges as a vital tool. Read moreabout What is Google Consent Mode V2? How to Implement It?