Skip to content

Cybersecurity · Web Security

HTTP Security Headers in 2026: The Checklist That Actually Matters

Most web apps are missing four or five headers that would neutralize entire classes of attack. Here's what each header does, what to set, and why most defaults leave you exposed.

Anurag Verma

Anurag Verma

6 min read

HTTP Security Headers in 2026: The Checklist That Actually Matters

Sponsored

Share

Security headers are one of those things where you can get from “totally unprotected” to “covers the most common attacks” in an afternoon. They’re not glamorous. They’re not a new technology. But a surprising fraction of production web apps are missing them entirely, and the ones that have them often have them configured wrong.

This is the header checklist worth running through every project before launch.

Content-Security-Policy

CSP is the most important and most complex header. It tells the browser which sources are allowed to load scripts, styles, images, fonts, and other resources. A tight CSP policy makes XSS attacks significantly harder to exploit even when an injection vulnerability exists.

A minimal CSP for a server-rendered app with no external scripts:

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

For apps that load scripts from CDNs or use inline styles:

Content-Security-Policy: 
  default-src 'self';
  script-src 'self' 'nonce-{RANDOM_NONCE}' https://cdn.example.com;
  style-src 'self' 'nonce-{RANDOM_NONCE}' https://fonts.googleapis.com;
  img-src 'self' data: https:;
  font-src 'self' https://fonts.gstatic.com;
  connect-src 'self' https://api.yourapp.com;
  frame-ancestors 'none';
  upgrade-insecure-requests;

The nonce approach is safer than hash-based or 'unsafe-inline'. Generate a cryptographically random nonce per request and inject it into both the header and any inline script/style tags.

// Next.js middleware example
import { NextResponse } from "next/server";
import { randomBytes } from "crypto";

export function middleware(request: Request) {
  const nonce = randomBytes(16).toString("base64");
  const response = NextResponse.next();
  
  response.headers.set(
    "Content-Security-Policy",
    `script-src 'self' 'nonce-${nonce}'; style-src 'self' 'nonce-${nonce}';`
  );
  response.headers.set("x-nonce", nonce); // pass to layout
  return response;
}

Start with report-only mode. Content-Security-Policy-Report-Only sends violation reports to your endpoint without blocking anything. Deploy this first, collect violations for a week, and use them to fix your policy before switching to enforcement mode:

Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-violations;

Strict-Transport-Security (HSTS)

HSTS tells browsers to only connect to your domain over HTTPS, and to remember this for a specified period. Once a user visits your site with a valid HSTS header, their browser will refuse to make HTTP connections to your domain for the duration of the max-age.

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
  • max-age=31536000: browsers remember for one year
  • includeSubDomains: applies to all subdomains too
  • preload: opt into the browser preload list (ships with browsers; protects first-time visitors)

The preload flag is a long-term commitment. Once your domain is on the HSTS preload list, removing it takes months. Only add preload if you’re confident all subdomains will serve HTTPS permanently.

Common mistake: setting HSTS on a subdomain that might return to HTTP. If you include includeSubDomains and have an internal tool at internal.yourapp.com that doesn’t have HTTPS configured, browsers will refuse to connect to it after one HTTPS visit.

X-Content-Type-Options

X-Content-Type-Options: nosniff

This one-liner prevents browsers from MIME-type sniffing. Without it, a browser might decide a text file “looks like JavaScript” and execute it, even if your server sent it with text/plain. With nosniff, browsers respect the Content-Type you set.

Always include this. There’s no configuration needed, no tradeoff, no compatibility issue.

X-Frame-Options and frame-ancestors

Clickjacking attacks embed your app in an iframe on an attacker’s page and trick users into clicking elements they can’t see. The old protection was X-Frame-Options:

X-Frame-Options: DENY
# or
X-Frame-Options: SAMEORIGIN

The modern replacement is CSP’s frame-ancestors directive:

Content-Security-Policy: frame-ancestors 'none';
# or
Content-Security-Policy: frame-ancestors 'self';

frame-ancestors is more flexible (you can allowlist specific origins) and is supported by all modern browsers. If you’re setting a CSP, use frame-ancestors there and skip the separate X-Frame-Options header. If you’re not setting CSP yet, use X-Frame-Options as a fallback.

Referrer-Policy

The Referer header leaks the URL the user came from to any external server the browser connects to. If your app has authenticated pages or query parameters with sensitive data in URLs, this is a real privacy issue.

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

This sends the full URL on same-origin requests (useful for analytics) and only the origin on cross-origin requests. It’s a reasonable default that doesn’t break most analytics setups while preventing URL leakage to third parties.

For apps with sensitive URL patterns, no-referrer or same-origin is safer:

Referrer-Policy: same-origin

Permissions-Policy

Permissions-Policy (formerly Feature-Policy) controls access to browser APIs. If your app doesn’t use the camera, microphone, or geolocation, telling the browser that directly prevents a compromised third-party script from accessing them.

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

An empty () means the feature is blocked for all origins. List only what you actually use:

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

This is lower priority than CSP and HSTS, but it’s easy to add and meaningful for apps that handle sensitive data.

Cross-Origin Headers: COEP, COOP, CORP

These three headers work together to enable cross-origin isolation, which is required for high-resolution timers, SharedArrayBuffer, and certain performance features. They also isolate your app from Spectre-style attacks.

Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Resource-Policy: same-origin

Warning: these headers can break apps that embed third-party content (iframes, images, fonts) unless those resources explicitly opt in with Cross-Origin-Resource-Policy: cross-origin. Enable these only after testing that your app loads correctly with them set.

For apps that don’t use SharedArrayBuffer and don’t need the highest-precision timers, these headers are optional. Start with the other headers and add these only if the features they enable are needed.

A Quick Config Reference

For Express:

app.use((req, res, next) => {
  res.setHeader("X-Content-Type-Options", "nosniff");
  res.setHeader("X-Frame-Options", "DENY");
  res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
  res.setHeader("Permissions-Policy", "camera=(), microphone=(), geolocation=()");
  res.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
  next();
});

For Next.js in next.config.js:

const securityHeaders = [
  { key: "X-Content-Type-Options", value: "nosniff" },
  { key: "X-Frame-Options", value: "DENY" },
  { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
  { key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" },
  { key: "Strict-Transport-Security", value: "max-age=31536000; includeSubDomains" },
];

module.exports = {
  async headers() {
    return [
      {
        source: "/(.*)",
        headers: securityHeaders,
      },
    ];
  },
};

For Nginx:

add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options DENY always;
add_header Referrer-Policy strict-origin-when-cross-origin always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

Testing Your Headers

securityheaders.com will scan any public URL and grade your header configuration. Run this on every production site before launch and any time you change your server configuration. It’s fast and the output is clear about what’s missing and why it matters.

The headers you want are:

  1. Strict-Transport-Security (HTTPS enforcement)
  2. Content-Security-Policy (XSS protection)
  3. X-Content-Type-Options: nosniff (MIME sniffing prevention)
  4. Referrer-Policy (URL leak prevention)
  5. Permissions-Policy (browser API access control)

You can add all of these in an afternoon on any app. The difficulty isn’t in setting the headers; it’s in getting the CSP policy right without breaking your own app. Start with report-only, fix violations, then enforce.

Sponsored

Sponsored

Discussion

Join the conversation.

Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.

Sponsored