DEV Community

Cover image for How to Audit HTTP Security Headers (CSP, HSTS & More)
Muhammad Ali Mughal
Muhammad Ali Mughal

Posted on

How to Audit HTTP Security Headers (CSP, HSTS & More)

Deploying a valid TLS certificate encrypts traffic in transit, but encryption alone does not stop a browser from executing injected cross-site scripting (XSS) payloads, loading your checkout page inside a hidden clickjacking iframe, or leaking sensitive URL tokens in Referer headers. Those browser-side guardrails are controlled entirely by HTTP response headers. Yet when we inspect production web apps after a deployment or domain cutover, HTTP security headers are frequently missing or silently stripped by a child location block. Below is a practical engineering guide to auditing your HTTP response headers in 10 minutes, configuring the six essential headers in Nginx and Node/Express, and rolling out a strict Content-Security-Policy (CSP) without breaking third-party scripts.

How to Audit HTTP Security Headers (CSP, HSTS & More)

Why Valid HTTPS Is Only Half of Browser Transport Security

As I covered in my earlier guides on how to check an SSL certificate in 10 minutes and fix common HTTPS errors and how to audit DNS records before a domain migration, infrastructure migrations often focus on DNS resolution and port 443 TLS handshakes. However, even with TLS 1.3 enabled, a user who types example.com into their address bar still initiates their very first request over plain http:// on port 80 unless their browser has cached a Strict-Transport-Security (HSTS) policy.

Even after the HTTPS redirect completes, modern browsers rely on explicit response headers from your origin server or CDN edge to know:

  • Which script, style, connect, and frame origins are trusted (Content-Security-Policy)
  • Whether another site is allowed to embed your application inside an <iframe> (X-Frame-Options and CSP frame-ancestors)
  • Whether the browser may guess ("sniff") executable JavaScript out of an uploaded file labeled text/plain (X-Content-Type-Options)
  • Which hardware APIs (camera, microphone, geolocation, payment) embedded scripts are allowed to invoke (Permissions-Policy)

You can inspect all of these headers in one request from your terminal or by running your live URL through the PixelValut HTTP Security Headers Checker.

Comparison Table: The 6 Essential HTTP Security Headers in 2026

Bookmark this reference table when auditing any production web application or API gateway:

Header Name Attack Mitigated Recommended Production Value Common Misconfiguration Pitfall
Strict-Transport-Security (HSTS) SSL stripping & first-visit http:// downgrade attacks max-age=63072000; includeSubDomains; preload Adding includeSubDomains when a legacy internal subdomain still runs on plain HTTP
Content-Security-Policy (CSP) Cross-Site Scripting (XSS), malicious script injection & clickjacking default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none' (plus vetted origins) Leaving 'unsafe-eval' and wildcard https: in script-src, which renders CSP ineffective
X-Content-Type-Options MIME-type sniffing of user uploads into executable HTML/JS nosniff Omitting the header on static asset buckets (S3/CDN) that serve user-uploaded files
X-Frame-Options UI redressing / clickjacking in older browsers DENY or SAMEORIGIN Setting ALLOW-FROM (deprecated and ignored by modern browsers; use CSP frame-ancestors instead)
Referrer-Policy Leaking session tokens, reset links, or private paths via Referer strict-origin-when-cross-origin Using unsafe-url, which sends the full URL query string to every external link and analytics pixel
Permissions-Policy Unauthorized access to browser sensors & APIs by third-party scripts camera=(), microphone=(), geolocation=(), payment=() Using the deprecated Feature-Policy header syntax instead of structured Permissions-Policy tuples

Step 1: Inspect Your Live Response Headers with curl

Before editing any server blocks, inspect the exact headers returned on your final HTTPS destination (-L follows redirects, -I sends a HEAD request):

curl -sIL https://your-domain.com | grep -iE "^(http/|strict-transport|content-security|x-frame|x-content-type|referrer-policy|permissions-policy|server:|x-powered-by:)"
Enter fullscreen mode Exit fullscreen mode

Look at two things in the output:

  1. Are security headers only present on the 301 redirect or on the final HTTP/2 200 response? Note that browsers ignore Strict-Transport-Security headers sent over plain http://—HSTS must be returned on the https:// response.
  2. Are information-disclosure headers leaking version numbers? If you see Server: nginx/1.24.0 or X-Powered-By: PHP/8.3.6, add server_tokens off; in Nginx and expose_php = Off in php.ini (or app.disable('x-powered-by') in Express).

Step 2: How to Roll Out a Content Security Policy (CSP) Without Breaking Production

Content-Security-Policy is the most powerful header in web security—and the easiest one to break a frontend with if you deploy it blindly. If your marketing team uses Google Tag Manager, Stripe Elements, or Cloudflare Turnstile, a naive default-src 'self' will block their scripts immediately.

The safe engineering workflow has three phases:

  1. Generate a Baseline Policy: List every legitimate domain your frontend loads scripts, styles, fonts, images, XHR/fetch requests (connect-src), and iframes (frame-src) from. You can build and validate the exact directive syntax interactively using the PixelValut CSP Generator.
  2. Deploy in Report-Only Mode First: Instead of enforcing Content-Security-Policy on day one, send Content-Security-Policy-Report-Only. Browsers will execute all page resources normally while logging any policy violations to the DevTools console and your reporting endpoint.
  3. Switch to Enforcing Mode: Once 48–72 hours of production traffic show zero false-positive violations, rename the header from Content-Security-Policy-Report-Only to Content-Security-Policy.

Step 3: Production-Ready Nginx Configuration (And Avoiding the add_header Inheritance Trap)

One of the most common Nginx bugs in production is silent header dropping. In Nginx, if you define add_header directives at the server block level, and then add even a single add_header inside a nested location block (such as add_header Cache-Control "public, immutable"; for static assets or a FastCGI block), Nginx drops all parent add_header directives for that location.

To fix this cleanly, keep your security headers in a dedicated snippet file (/etc/nginx/snippets/security-headers.conf) and include it in every location block that defines its own headers—or use the headers-more-nginx-module:

# /etc/nginx/snippets/security-headers.conf
server_tokens off;

# 1. Force HTTPS for 2 years across all subdomains
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

# 2. Prevent MIME-sniffing & clickjacking
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;

# 3. Control Referer leakage & browser hardware APIs
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), interest-cohort=()" always;

# 4. Content Security Policy (adjust origins for your app)
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://js.stripe.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://api.stripe.com; frame-src https://js.stripe.com; frame-ancestors 'self'; object-src 'none'; base-uri 'self';" always;
Enter fullscreen mode Exit fullscreen mode

Always include the always parameter at the end of each add_header directive so Nginx sends the security headers on 4xx and 5xx error responses as well as 200 OK responses.

The 10-Minute HTTP Security Header Audit Checklist

Run through this checklist after every infrastructure change, CDN update, or framework upgrade:

  • [ ] Verify HSTS on the https:// Response: Confirm Strict-Transport-Security has a max-age of at least 31536000 (1 year), and only enable includeSubDomains after verifying every subdomain supports HTTPS.
  • [ ] Check Both HTML Routes and Static/API Endpoints: Test your homepage (/), an authenticated API route (/api/v1/health), and a static asset (/assets/app.js) to ensure nested Nginx location blocks did not strip your security headers.
  • [ ] Eliminate X-XSS-Protection: 1; mode=block: Modern browsers have removed their legacy XSS Auditor because it introduced cross-site leaks; either omit X-XSS-Protection or set it to 0 while relying on a real Content-Security-Policy.
  • [ ] Lock Down frame-ancestors and X-Frame-Options: Set both X-Frame-Options: DENY (or SAMEORIGIN) and CSP frame-ancestors 'none' (or 'self') so neither modern nor legacy browsers allow third-party iframe embedding.
  • [ ] Strip Server and X-Powered-By Banners: Remove exact web server and runtime version strings from response headers.
  • [ ] Run an Automated External Header Scan: Test your live domain with the PixelValut Security Headers Checker to verify every header directive and grade in seconds.

Frequently Asked Questions (FAQ)

Do I still need X-Frame-Options if I have frame-ancestors in my CSP?

Yes, keeping X-Frame-Options: DENY (or SAMEORIGIN) alongside CSP frame-ancestors is recommended for defense-in-depth. Modern browsers prioritize CSP frame-ancestors when both are present, while legacy clients and older embedded webviews still fall back to X-Frame-Options.

Why did my Nginx security headers disappear on my /static/ or /storage/ routes?

Because of Nginx's add_header inheritance rule: any location block that declares its own add_header (for example, add_header Cache-Control "public, max-age=31536000";) completely overrides all add_header statements defined in the parent server block. Move your security headers into a reusable snippet and include that snippet inside any location block that sets custom caching or CORS headers.

What is the safest max-age when turning on HSTS for the first time?

When enabling HSTS on a domain for the very first time, start with a short max-age=300 (5 minutes) or max-age=86400 (24 hours) without preload. Once you verify that no subdomains or internal tools break over HTTPS, increase max-age to 31536000 (1 year) or 63072000 (2 years).

Should JSON REST APIs return a Content-Security-Policy header?

Yes. Even if an API endpoint is only meant to return application/json, returning Content-Security-Policy: default-src 'none'; frame-ancestors 'none' along with X-Content-Type-Options: nosniff prevents browsers from ever rendering an API response as an HTML document or framing it if a route misbehaves.

How can I build a custom CSP header without memorizing every directive name?

Use the free PixelValut CSP Generator to select your allowed script, style, image, font, and connect sources visually and copy the ready-to-use Nginx, Apache, or Cloudflare header string.

Top comments (0)