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-ancestorsdirective (which is part of CSP and supersedes the olderX-Frame-Optionsheader). - 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, andframe-ancestors 'self'. Keep reporting on. - Turn on HTTP Strict Transport Security after fixing HTTPS. Use
max-age=31536000; includeSubDomains. Addpreloadonly 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. Useno-referrerif you handle sensitive data. - Enable cross-origin isolation only when needed. Use
COOP: same-originandCOEP: require-corpor credentialless. Add CORP on your assets. - Always send
X-Content-Type-Options: nosniff. - Lock cookies with
Secure; HttpOnly; SameSite=LaxorStrict. UseSameSite=None; Secureonly for third-party cookies. - Use CORS allowlists. Never combine
*with credentials. Cache preflight with a shortAccess-Control-Max-Age. - Add Fetch Metadata checks on state-changing endpoints. Reject
cross-siteunsafe 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-requestsReport-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-legacyReporting 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-urifor legacy browsers. - Use
report-tofor 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-Onlyon 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-srcto unknown hosts. - Rotate nonces and do not allow
'unsafe-inline'or'unsafe-eval'.
CSP comparison
| Name | Purpose | Critical directives | Example value | Browser support | Breakage risk | Tooling notes |
|---|---|---|---|---|---|---|
| Content-Security-Policy | Client-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-ancestors | default-src, script-src with nonce, frame-ancestors,
object-src 'none', upgrade-insecure-requests | See policy above | Chrome 25+, Edge 12+, Firefox 23+, Safari 7+, Opera 15+, iOS Safari 6+, IE 10-11 partial via
X-Content-Security-Policy | Medium 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 working | Use Reporting API and keep report-uri while migrating |
HTTP Strict-Transport-Security (HSTS)
Purpose: force HTTPS. Block HTTP and invalid TLS bypass attempts.
Recommended value
Strict-Transport-Security: max-age=31536000; includeSubDomainsPreload
- Only add
preloadwhen 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-ageto at least 31536000 and includeincludeSubDomainsfor two weeks before submitting. - Document a rollback plan. Preload removal takes time and cannot be done instantly.
HSTS comparison
| Name | Purpose | Critical directives | Example value | Browser support | Breakage risk | Tooling notes |
|---|---|---|---|---|---|---|
| HSTS | Transport security policy that instructs user agents to auto-upgrade HTTP to HTTPS and pin HTTPS-only for the origin, preventing protocol downgrade and SSL stripping | max-age, includeSubDomains, optional preload | max-age=31536000; includeSubDomains | Chrome 4+, Edge 12+, Firefox 4+, Safari 7+, Opera 12+, IE 11 on Windows 8.1 or Windows 7 with KB3058515 | High if any subdomain or legacy endpoint still serves HTTP or presents invalid TLS, as the browser will hard-fail those navigations | Test redirects and certs first |
Referrer-Policy
Purpose: control how much referrer info you send.
Default
Referrer-Policy: strict-origin-when-cross-originUse no-referrer for maximum privacy.
Referrer-Policy comparison
| Name | Purpose | Critical directives | Example value | Browser support | Breakage risk | Tooling notes |
|---|---|---|---|---|---|---|
| Referrer-Policy | Request privacy control that bounds the Referer header granularity across same-origin and cross-origin navigations and subresource fetches, reducing origin and path leakage | One token like no-referrer, strict-origin-when-cross-origin | strict-origin-when-cross-origin | Chrome 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 visibility | Check 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=()andusb=()for public marketing pages.fullscreen=(self)for apps that use media viewers.
Permissions-Policy comparison
| Name | Purpose | Critical directives | Example value | Browser support | Breakage risk | Tooling notes |
|---|---|---|---|---|---|---|
| Permissions-Policy | Feature gating policy that allows or denies powerful browser capabilities per origin and embedding context, enforcing least privilege for APIs like geolocation, camera, and microphone | Per-feature allowlists | geolocation=(), camera=(), microphone=() | Chrome 88+, Edge 88+, Opera 74+, Firefox not supported, Safari not supported | Low to medium. Pages or iframes that implicitly relied on prompts will see capability denials until explicitly allowed | Set 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: nosniffX-Content-Type-Options comparison
| Name | Purpose | Critical directives | Example value | Browser support | Breakage risk | Tooling notes |
|---|---|---|---|---|---|---|
| X-Content-Type-Options | MIME enforcement directive that disables content sniffing for script and style responses, requiring a
correct Content-Type to execute | nosniff | nosniff | Chrome 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 blocked | Serve 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.
Recommended set when you need isolation
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-originNotes
COEP: credentiallesscan 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-originon your assets or serve them from the same origin. - Third-party widgets may not embed under COEP. Host them
first-party or use
credentiallessif supported.
COOP, COEP, CORP comparison
| Name | Purpose | Critical directives | Example value | Browser support | Breakage risk | Tooling notes |
|---|---|---|---|---|---|---|
| COOP | Browsing context group isolation that places the page in a separate group from cross-origin documents, mitigating cross-window data leaks and certain XS-Leaks | same-origin | same-origin | Chrome 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 break | Audit postMessage and window.open |
| COEP | Embedding policy that requires cross-origin resources to explicitly grant embedding via CORP or CORS, enabling cross-origin isolation and blocking opaque no-cors embeds | require-corp or credentialless | require-corp | Chrome 83+, Edge 83+, Opera 69+, Firefox not supported, Safari not supported | Medium to high. Third-party assets without CORP or CORS headers will fail to load, including fonts, images, and WASM | Add CORP on your assets |
| CORP | Resource protection policy that declares which origins may embed a resource in no-cors contexts, preventing unauthorized cross-origin consumption | same-origin, same-site, cross-origin | same-origin | Chrome 73+, Edge 79+, Firefox 74+, Safari not supported, iOS Safari not supported | Low to medium. Cross-site consumers without an allowed relationship will fail to load protected assets | Set 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-originPreflight
- 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: Originwhen responses differ by origin. - Allow only necessary methods and headers. Avoid broad
Access-Control-Allow-Headers: *.
Preflight caching gotchas
- Keep
Access-Control-Max-Agemodest 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
| Name | Purpose | Critical directives | Example value | Browser support | Breakage risk | Tooling notes |
|---|---|---|---|---|---|---|
| CORS headers | Cross-origin request authorization framework that governs which origins, methods, and headers may access protected resources via the fetch/XHR pipeline, including preflight negotiation | Access-Control-Allow-Origin, Access-Control-Allow-Methods,
Access-Control-Allow-Headers, Access-Control-Allow-Credentials,
Access-Control-Max-Age | See example | Chrome 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 mismatches | Always 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
| Name | Purpose | Critical directives | Example value | Browser support | Breakage risk | Tooling notes |
|---|---|---|---|---|---|---|
| Fetch Metadata | Request context validation that inspects Sec-Fetch-* headers to differentiate same-origin, same-site, and cross-site requests, enabling server-side CSRF and XS-Leak hardening | Sec-Fetch-Site, Sec-Fetch-Mode, Sec-Fetch-Dest,
Sec-Fetch-User | Reject unsafe methods when cross-site | Chrome 76+, Edge 79+, Opera 63+, Firefox not supported, Safari not supported | Low when scoped to unsafe methods. Overly strict rules can block legitimate cross-site navigations or OAuth redirects | Log 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
| Name | Purpose | Critical directives | Example value | Browser support | Breakage risk | Tooling notes |
|---|---|---|---|---|---|---|
| SRI | Subresource integrity verification that binds external scripts and styles to cryptographic digests, preventing execution of tampered CDN assets | integrity, crossorigin | See example | Chrome 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 failures | Automate hash updates |
Secure cookies
Purpose: protect sessions.
Safe example
Set-Cookie: session=abc...; Path=/; Secure; HttpOnly; SameSite=LaxNotes
- Modern browsers default to Lax when SameSite is missing. Be explicit.
- Use
SameSite=None; Securefor third-party cookies only.
Cookie scope tips
- Set
Path=/only when needed. Narrower paths reduce exposure. - Scope
Domainto 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
| Name | Purpose | Critical directives | Example value | Browser support | Breakage risk | Tooling notes |
|---|---|---|---|---|---|---|
| Set-Cookie | Session confidentiality and CSRF defense-in-depth via Secure transport, script-inaccessibility with HttpOnly, and cross-site semantics with SameSite | Secure, HttpOnly, SameSite | Secure; HttpOnly; SameSite=Lax | HttpOnly 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 required | Audit cross-site flows before tightening |
Obsolete or discouraged headers
| Header | Removed/Deprecated/etc. since when | Reason | Replacement |
|---|---|---|---|
| HTTP Public-Key-Pins (HPKP) | Removed, 2018-05-29, Risk of denial-of-service and hostile pinning | High risk of self-inflicted lockout | Replace Public-Key-Pins with HSTS
preload and Certificate Transparency |
| X-XSS-Protection | Deprecated, 2019-10, Deprecated by browsers, can create XSS vulnerabilities and conflicts with CSP | Ineffective and misleading | Replace X-XSS-Protection with Content-Security-Policy with no inline scripts |
| Expect-CT | Deprecated, 2022-11, CT enforced by Chromium, header obsolete | CT now built into ecosystem | Replace Expect-CT with rely on Certificate Transparency and remove header |
| X-Frame-Options | Superseded by modern control, 2016-12-15 reference for CSP frame-ancestors adoption, Limited directives
and interoperability | Limited syntax and control | Replace 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
HTMLRewriterin Cloudflare) to stream the response, inject thenonceattribute into your<script>and<style>tags, and then - Add the
Content-Security-Policyheader 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/nullSecurity 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.
grepfinds 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 Policyviolations while in Report-Only. - Firefox: Storage panel then Cookies. Check
Secure,HttpOnly, andSameSiteflags.
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 andVary: Origin. - Sending only X-Frame-Options. Use
frame-ancestorsin 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-ancestorsin CSP. Add it even if you also send X-Frame-Options. - COEP
require-corpdeployed without CORP on static assets. AddCross-Origin-Resource-Policyon images, fonts, and WASM. - CORS
Access-Control-Allow-Headersset to*. Replace with exact headers used. - Cookies with
SameSite=Nonebut missingSecure. Always pairNonewithSecure.
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
- MDN HTTP header reference (opens in new window)
- MDN CSP guide (opens in new window)
- W3C Content Security Policy Level 3 (opens in new window)
- MDN HSTS (opens in new window)
- OWASP HSTS Cheat Sheet (opens in new window)
- MDN Referrer-Policy (opens in new window)
- MDN Permissions-Policy (opens in new window)
- MDN X-Content-Type-Options (opens in new window)
- MDN COOP (opens in new window)
- MDN COEP (opens in new window)
- MDN CORP (opens in new window)
- MDN CORS guide (opens in new window)
- WICG Fetch Metadata (opens in new window)
- MDN CSRF overview (opens in new window)
- MDN SRI (opens in new window)
- W3C SRI (opens in new window)
- OWASP Secure Headers Project (opens in new window)
- OWASP HTTP Headers Cheat Sheet (opens in new window)
- Mozilla HTTP Observatory (opens in new window)
- SecurityHeaders.com (opens in new window)
- OWASP ZAP (opens in new window)
Let’s amplify your success together!
Request a Free QuoteRelated articles

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

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()

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?
