All articles
Application Security

Security Headers Every Production Website Should Send

Stop failing penetration tests. Use this practical production checklist for HTTP security headers, including HSTS, CSP, nosniff, Referrer-Policy, and what old headers you should remove.

Gloria Garcia·AppSec Engineer
·16 min read
Share:

The backend is solid. Auth flows are tested. The deployment pipeline is green. A security engineer runs an external penetration test or an enterprise procurement team runs a compliance scan — and the report comes back flagging a dozen issues under “missing or misconfigured HTTP response headers.”

This scenario happens constantly. The application code may be well-defended, but the browser-facing security policy layer — the set of HTTP response headers that instruct browsers how to behave when loading your site — is either missing, outdated, or overridden somewhere between your application server and the user’s browser.

Security headers are not optional cosmetic signals. They are the mechanism that tells browsers to enforce HTTPS, block clickjacking attempts, prevent MIME type confusion, restrict script execution, and limit how much referrer information leaks to third parties. Without them, the browser uses permissive defaults — and permissive defaults are what attackers exploit.

TL;DR

  • Production sites should send a modern header baseline, not rely on framework defaults alone.
  • HSTS enforces HTTPS at the browser level.
  • X-Content-Type-Options: nosniff prevents MIME sniffing attacks.
  • Referrer-Policy reduces unnecessary URL leakage to third parties.
  • Content-Security-Policy is the heavyweight control for script and resource restrictions.
  • X-XSS-Protection is deprecated and should not be treated as a modern security measure.
  • Headers must be verified on the live production edge, not assumed from local dev configuration.

Already configured your headers? Check the live response your users actually receive before assuming your proxy or CDN preserved them: CodeAva HTTP Headers Checker.

The production reality: what you configured is not always what ships

Setting headers in your application code is necessary but not sufficient. Between your application and the browser sits an entire chain of infrastructure — and any layer in that chain can modify what the user receives.

  • Reverse proxies (Nginx, Apache, Caddy, Traefik) can override or duplicate headers set by the upstream application.
  • CDNs (Cloudflare, Fastly, AWS CloudFront, Akamai) may add, strip, or cache headers based on their own policies.
  • WAFsand platform middleware (Vercel, Netlify, AWS ALB) sometimes inject their own security headers — or remove yours.
  • Load balancers can deduplicate or merge conflicting header values, producing unexpected results.

The result is that a header you set correctly in Express, Django, or Next.js middleware may never reach the browser. Or it may arrive duplicated with conflicting values, which browsers handle inconsistently.

Configuration drift is real

The most dangerous assumption in production security is that the headers you configured locally are the headers your users receive. Always verify from the outside in — from a tool or browser that hits your live production URL, not your local dev server.

This is why the final step of any header hardening effort must be external verification. Set the headers in your application, then confirm they survive the full infrastructure chain by inspecting the actual response.

The modern baseline: headers most production websites should send

The following headers represent a practical baseline for modern production websites. Not every site needs every header with the strictest possible value, but most production sites should be sending all of these with reasonable defaults.

Strict-Transport-Security (HSTS)

HSTS tells browsers: “Always use HTTPS when talking to this domain.” After the browser sees this header over HTTPS, it will automatically upgrade all future HTTP requests to HTTPS for the specified duration — even if the user types http:// manually.

Strict-Transport-Security: max-age=31536000

The max-age value is in seconds. 31536000 is one year. You can optionally add includeSubDomains to apply the policy to all subdomains:

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

Only send HSTS over HTTPS.If sent over plain HTTP, the browser must ignore it. This is by design — an attacker on an insecure connection could inject a fake HSTS header.

About preload: The preload directive asks browser vendors to include your domain in a hardcoded HSTS list shipped with the browser itself. This means even the first visit is HTTPS, before the browser ever sees your HSTS header. This is a high-commitment step. Once your domain is in the preload list, removing it takes months. Do not add preload unless you are confident HTTPS works correctly across all subdomains and you understand the commitment.

X-Content-Type-Options

This header tells the browser to trust the Content-Type header and not attempt to guess (sniff) the MIME type of the response based on its content.

X-Content-Type-Options: nosniff

Without this header, a browser might interpret a text file or JSON response as HTML if it contains HTML-like content. This can turn a file-upload or API endpoint into an XSS vector. The only valid value is nosniff. There is no reason not to send it on every response.

Referrer-Policy

Controls how much URL information the browser sends in the Referer header when navigating away from your site or loading third-party resources.

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

This value sends the full URL for same-origin requests, sends only the origin (scheme + domain) for cross-origin HTTPS-to-HTTPS requests, and sends nothing for HTTPS-to-HTTP downgrades. It is the practical default for most production sites.

More restrictive options include no-referrer (never send any referrer) and same-origin (only send referrer for same-origin requests). Choose based on your analytics and third-party integration needs.

X-Frame-Options & CSP frame-ancestors

Both headers prevent clickjacking by controlling whether your page can be loaded inside an <iframe>, <frame>, or <object>.

X-Frame-Options: DENY

X-Frame-Options is the legacy header. It supports only two practical values: DENY (no framing at all) and SAMEORIGIN (only the same origin can frame).

The modern replacement is the frame-ancestors directive inside Content-Security-Policy:

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

frame-ancestorsis more flexible — it supports specific origins, wildcards, and scheme restrictions. If both X-Frame-Options and frame-ancestors are present, browsers that support CSP will use frame-ancestors and ignore X-Frame-Options.

Backward compatibility

Sending both X-Frame-Options: DENY and Content-Security-Policy: frame-ancestors 'none' provides protection for older browsers that do not support CSP frame-ancestors. If you need to allow framing from specific origins, use frame-ancestors only — X-Frame-Options cannot express origin-level allowlists.

Content-Security-Policy (CSP)

CSP is the single most powerful security header. It tells the browser exactly which sources are allowed to load scripts, styles, images, fonts, frames, and other resources. A well-configured CSP dramatically reduces the impact of cross-site scripting vulnerabilities.

A minimal starting policy might look like:

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

A strict, modern CSP uses nonces or hashes instead of domain allowlists:

Content-Security-Policy: default-src 'self'; script-src 'nonce-{random}' 'strict-dynamic'; style-src 'self' 'nonce-{random}'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'

CSP is also the most complex security header to get right. Inline scripts, third-party analytics, tag managers, and dynamically loaded resources all require careful handling. For a deep implementation guide specific to React and Next.js, see the CSP for React and Next.js guide.

If you are unsure whether your existing CSP is effective, the CodeAva CSP Evaluator can parse and analyze your policy.

Permissions-Policy

Permissions-Policy (formerly Feature-Policy) controls which browser features your site and any embedded iframes can use. It covers APIs like camera, microphone, geolocation, payment, and more.

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

The empty parentheses ()mean “denied for all origins, including this one.” This is the most restrictive setting. If your site needs any of these features, list the allowed origins explicitly:

Permissions-Policy: camera=(self "https://meet.example.com"), microphone=(self)

This header is especially valuable for sites that embed third-party content. It prevents an embedded iframe from requesting permissions that the host page did not explicitly grant.

Baseline security headers checklist

Use this table as a quick-reference checklist. Every production website should send these headers with at least the suggested baseline value. Adjust based on your application requirements.

HeaderBaseline valueWhat it prevents
Strict-Transport-Securitymax-age=31536000HTTP downgrade attacks, SSL stripping
X-Content-Type-OptionsnosniffMIME sniffing, content type confusion
Referrer-Policystrict-origin-when-cross-originURL leakage to third parties
X-Frame-OptionsDENYClickjacking (legacy browsers)
Content-Security-Policydefault-src 'self'; frame-ancestors 'none'XSS, data injection, clickjacking
Permissions-Policycamera=(), microphone=(), geolocation=()Unwanted feature access by embedded content

Contextual and advanced headers

The headers below are valuable in specific contexts but are not universal baseline requirements. Add them when your deployment architecture warrants them.

Cross-Origin-Opener-Policy (COOP)

Controls whether your page shares a browsing context group with cross-origin windows opened via window.open or popups. Setting it to same-origin isolates your page from cross-origin openers, preventing certain Spectre-class side-channel attacks.

Cross-Origin-Opener-Policy: same-origin

Required (along with COEP) to enable SharedArrayBuffer and high-resolution timers. If your application does not use these features, it is still a sensible hardening measure, but test carefully — it can break OAuth popup flows and cross-origin window communication.

Cross-Origin-Embedder-Policy (COEP)

Requires all subresources to either be same-origin or explicitly opt into cross-origin loading via CORS headers. Together with COOP, it enables cross-origin isolation.

Cross-Origin-Embedder-Policy: require-corp

This header can break third-party images, fonts, and scripts that do not send appropriate CORS headers. Use credentialless as a less strict alternative if full isolation is not needed.

Cross-Origin-Resource-Policy (CORP)

Controls whether other origins can load your resources. Useful for preventing your images, scripts, or API responses from being embedded by untrusted sites.

Cross-Origin-Resource-Policy: same-origin

Cache-Control for sensitive responses

For pages behind authentication or containing user-specific data, set aggressive Cache-Controlto prevent shared caches from serving one user’s data to another.

Cache-Control: no-store, no-cache, must-revalidate, private

This is not a security header per se, but misconfigured caching is a common source of data leakage in production.

Headers you should stop sending

Legacy headers that once provided value but now either do nothing or actively cause problems. Remove them from your production configuration.

X-XSS-Protection

This header enabled the browser’s built-in XSS auditor. Chrome removed the XSS auditor in version 78 (October 2019). Firefox never implemented it. No modern browser supports it.

# Do NOT send this header
X-XSS-Protection: 1; mode=block

Worse, the mode=block value historically introduced information leakage vulnerabilities in some browsers. The 1; mode=block setting could be weaponized to detect whether certain content existed on a page.

The replacement is CSP. A strict Content Security Policy provides actual XSS mitigation. X-XSS-Protection provides none.

Remove, do not just ignore

If your scanner flags X-XSS-Protection as missing, the correct response is to explain it is intentionally not sent, not to add it. Some older compliance checklists still reference it — the standard has moved on.

X-Powered-By / Server

These headers expose your technology stack to anyone who inspects response headers. They provide no benefit to the browser and give attackers free reconnaissance.

# Remove these
X-Powered-By: Express
Server: Apache/2.4.51

In Express, call app.disable('x-powered-by'). In Nginx, set server_tokens off. In Apache, use ServerTokens Prod and ServerSignature Off. Most CDNs and hosting platforms allow you to strip these headers in configuration.

Public-Key-Pins (HPKP)

HTTP Public Key Pinning was removed from all major browsers due to the risk of accidental permanent denial of service. Chrome removed support in version 72. Do not send it.

Send vs. remove: comparison table

HeaderVerdictReason
Strict-Transport-SecuritySendEnforces HTTPS at the browser level
X-Content-Type-OptionsSendPrevents MIME sniffing
Referrer-PolicySendControls URL leakage
Content-Security-PolicySendXSS mitigation, resource control
X-Frame-OptionsSend (+ frame-ancestors)Clickjacking protection; backward compat
Permissions-PolicySendRestricts browser APIs for embedded content
X-XSS-ProtectionRemoveRemoved from all browsers; can cause issues
X-Powered-ByRemoveFree reconnaissance for attackers
ServerRemove / minimizeExposes server software and version
Public-Key-PinsRemoveDeprecated; risk of permanent DoS

Step-by-step: implementing the baseline

Use this as a deployment checklist. The goal is to get the baseline headers deployed and verified before addressing advanced or contextual headers.

Step 1: Audit your current response headers

Before adding anything, check what your production site already sends. Use the CodeAva HTTP Headers Checker to inspect the live response headers for your production URL. Note which baseline headers are already present, which are missing, and whether any conflicting or duplicate values appear.

Step 2: Set headers at the right layer

Decide where in your infrastructure chain to set each header. For most teams, the reverse proxy or CDN edge is the most reliable place because it applies regardless of which backend service generates the response.

  • Application layer— best for CSP (especially nonce-based), because the nonce must be generated per-request by the application.
  • Reverse proxy / CDN— best for HSTS, nosniff, Referrer-Policy, X-Frame-Options, and Permissions-Policy, because these values are static and should apply to all responses.

Step 3: Add baseline headers incrementally

Start with the least-risky headers first and work up to CSP, which is the most likely to break something:

  1. X-Content-Type-Options: nosniff— no risk, deploy immediately.
  2. Referrer-Policy: strict-origin-when-cross-origin — low risk, minor analytics impact.
  3. X-Frame-Options: DENY— unless your site is intentionally framed somewhere.
  4. Strict-Transport-Security: max-age=31536000— only over HTTPS. Start with a short max-age (e.g. 300) and increase after verifying.
  5. Permissions-Policy— deny unused features.
  6. Content-Security-Policy— deploy in report-only mode first (Content-Security-Policy-Report-Only), fix violations, then enforce.

Step 4: Remove legacy headers

Strip X-XSS-Protection, X-Powered-By, detailed Server values, and Public-Key-Pinsfrom your responses. Check both your application and your infrastructure layers — these headers are often set by defaults you did not configure.

Step 5: Verify from the outside

After deploying, verify the live response from outside your network. Use the HTTP Headers Checker or a curl -Icommand to confirm every header arrives as expected. Check all key routes — the homepage, API endpoints, static assets, and authenticated pages may have different header sets.

Step 6: Automate ongoing verification

Security headers drift over time as infrastructure changes. Add a CI step or a scheduled health check that validates your production response headers against expected values. If your team runs periodic penetration tests or uses a website security audit, include header verification in the scope.

CSP deserves a separate implementation plan

Of all the headers in this guide, CSP is the one most likely to break your site if misconfigured. Inline scripts, third-party analytics tags, ad networks, chat widgets, embedded videos, and dynamically loaded modules all need to be accounted for in the policy.

A common approach:

  1. Start with report-only: Deploy Content-Security-Policy-Report-Only with a report-uri or report-to endpoint. This logs violations without blocking anything.
  2. Analyze violations: Review reports for a few days to identify all legitimate scripts and resources that would be blocked.
  3. Refine the policy: Allowlist legitimate sources using nonces, hashes, or specific origins. Eliminate 'unsafe-inline' and 'unsafe-eval' wherever possible.
  4. Enforce: Switch from report-only to enforced CSP. Keep reporting active to catch new violations.

For a deep dive into nonce-based CSP for React and Next.js, see Content Security Policy (CSP) for React and Next.js. For policy analysis, use the CSP Evaluator & Violation Parser.

Why production verification is the most important step

The entire purpose of security headers is to instruct the browser. If the browser does not receive the header, the instruction does not exist. It does not matter what your application code sets, what your Nginx config says, or what your CDN documentation promises — the only header that matters is the one the browser actually sees.

This is why the workflow is: configure, deploy, then verify from the outside. The CodeAva HTTP Headers Checker fetches your URL as a real browser would, showing you every response header including security header analysis. It catches issues that local testing misses: stripped headers, CDN overrides, duplicate values, and conflicting directives.

Check before and after every infrastructure change

Verify your headers after deploying new middleware, changing CDN providers, updating reverse proxy configurations, or enabling new platform features. Any of these can silently change what headers reach the browser.

Security headers are production hygiene

Security headers are not advanced security. They are production hygiene — the web equivalent of locking the doors and setting the alarm system. Most of the baseline headers in this guide are single-line additions with zero runtime cost. The effort required is small. The risk reduction is significant.

The checklist is straightforward: send HSTS, nosniff, Referrer-Policy, X-Frame-Options with frame-ancestors, Permissions-Policy, and a Content Security Policy. Remove X-XSS-Protection, X-Powered-By, verbose Server headers, and any HPKP remnants. Then verify what your users actually receive.

If you have not checked your production headers recently, start here: CodeAva HTTP Headers Checker →

#security-headers#HSTS#CSP#X-Content-Type-Options#Referrer-Policy#X-Frame-Options#Permissions-Policy#production-hardening#penetration-testing#HTTP#appsec

Frequently asked questions

More from Gloria Garcia

Found this useful?

Share:

Want to audit your own project?

These articles are written by the same engineers who built CodeAva\u2019s audit engine.