Web Security

HTTP Security Headers: The Complete Configuration Guide

Configure CSP, HSTS, X-Frame-Options, Permissions-Policy, and Referrer-Policy correctly with ready-to-use nginx, Express, and Next.js configuration examples.

March 9, 20266 min readShipSafer Team

HTTP Security Headers: The Complete Configuration Guide

HTTP security headers are one of the highest-leverage defenses available to web applications. A few lines of configuration protect against entire categories of attack — clickjacking, XSS, protocol downgrade attacks, and data leakage via referrers. Yet misconfigured or missing headers remain among the most common findings in web application security assessments.

This guide covers every header that matters, explains what it does, and provides correct configuration for nginx, Express, and Next.js.

Content-Security-Policy (CSP)

CSP is the most powerful security header and the most complex to configure correctly. It instructs the browser to only execute scripts, load styles, and make connections to sources you explicitly list, making XSS exploitation dramatically harder even when an injection vulnerability exists.

A solid baseline CSP for a Next.js application:

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

Key directives to understand:

  • default-src 'self' — fallback for all resource types not explicitly listed; restricts to same origin.
  • script-src 'nonce-...' — only scripts with a matching nonce attribute execute. Generate a fresh cryptographic nonce per request. Never use unsafe-inline or unsafe-eval in production.
  • frame-ancestors 'none' — supersedes X-Frame-Options; prevents your page from being embedded in any frame.
  • base-uri 'self' — prevents <base> injection, which can redirect all relative URLs.
  • upgrade-insecure-requests — tells the browser to load all subresources over HTTPS even if referenced via HTTP.

CSP in Next.js with nonces

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import crypto from 'crypto';

export function middleware(request: NextRequest) {
  const nonce = crypto.randomBytes(16).toString('base64');
  const csp = [
    `default-src 'self'`,
    `script-src 'self' 'nonce-${nonce}'`,
    `style-src 'self' 'nonce-${nonce}'`,
    `img-src 'self' data: blob:`,
    `connect-src 'self'`,
    `frame-ancestors 'none'`,
    `base-uri 'self'`,
    `form-action 'self'`,
  ].join('; ');

  const response = NextResponse.next();
  response.headers.set('Content-Security-Policy', csp);
  response.headers.set('x-nonce', nonce); // Pass to layout via headers
  return response;
}

HTTP Strict Transport Security (HSTS)

HSTS tells browsers to always use HTTPS for your domain, even if a user types http:// or clicks an HTTP link. It eliminates SSL-stripping attacks.

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
  • max-age=31536000 — cache this instruction for one year (the standard recommendation).
  • includeSubDomains — apply to all subdomains. Add this only once you are sure every subdomain is HTTPS-capable.
  • preload — opt into the browser preload list. Browsers ship with a hardcoded list of HSTS domains that never make an initial HTTP request. Submit your domain at hstspreload.org after adding this directive.

HSTS should only be sent over HTTPS responses. Sending it over HTTP is meaningless because an attacker can strip it.

X-Frame-Options

This legacy header prevents your page from being framed, defending against clickjacking. It is superseded by CSP frame-ancestors but should still be set for older browser compatibility.

X-Frame-Options: DENY

Use DENY unless you explicitly need to frame pages within your own origin, in which case use SAMEORIGIN. The third option ALLOW-FROM is not supported in modern browsers; use CSP frame-ancestors for per-origin exceptions.

X-Content-Type-Options

Prevents browsers from MIME-sniffing responses away from the declared Content-Type. This stops attacks where a server returns a file the browser sniffs as JavaScript and executes.

X-Content-Type-Options: nosniff

Always set this. It has no downside.

Permissions-Policy

Formerly Feature-Policy, this header disables browser APIs your application does not use. Restricting access to camera, microphone, and geolocation limits the damage from XSS.

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

Only list APIs you actually need. If your app uses geolocation legitimately:

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

Referrer-Policy

Controls how much URL information is included in the Referer header when navigating away from your page. Sensitive URL path information (user IDs, tokens in query strings, internal paths) can leak to third parties via referrer.

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

This is the recommended default. It sends the full URL for same-origin requests (useful for analytics) and only the origin for cross-origin requests (prevents path leakage).

Other useful values:

  • no-referrer — send nothing; most private, may break some analytics.
  • same-origin — send full URL only to same origin, nothing cross-origin.
  • strict-origin — send only origin (no path) on all requests, and nothing when downgrading to HTTP.

Complete Configuration Examples

nginx

server {
    # HSTS — only in the HTTPS server block
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

    # Framing
    add_header X-Frame-Options "DENY" always;
    add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; upgrade-insecure-requests;" always;

    # MIME sniffing
    add_header X-Content-Type-Options "nosniff" always;

    # Referrer
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # Permissions
    add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;

    # Remove information-leaking headers
    server_tokens off;
    more_clear_headers Server;
    more_clear_headers X-Powered-By;
}

Express (Node.js)

Use the helmet package, which sets sensible defaults for all the headers above:

import helmet from 'helmet';
import crypto from 'crypto';

app.use((req, res, next) => {
  res.locals.nonce = crypto.randomBytes(16).toString('base64');
  next();
});

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", (req, res) => `'nonce-${(res as any).locals.nonce}'`],
      styleSrc: ["'self'", (req, res) => `'nonce-${(res as any).locals.nonce}'`],
      imgSrc: ["'self'", 'data:'],
      connectSrc: ["'self'"],
      frameAncestors: ["'none'"],
      baseUri: ["'self'"],
      formAction: ["'self'"],
      upgradeInsecureRequests: [],
    },
  },
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true,
  },
  referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
  permittedCrossDomainPolicies: false,
}));

Next.js (next.config.ts)

import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          {
            key: 'Strict-Transport-Security',
            value: 'max-age=31536000; includeSubDomains; preload',
          },
          {
            key: 'X-Frame-Options',
            value: 'DENY',
          },
          {
            key: 'X-Content-Type-Options',
            value: 'nosniff',
          },
          {
            key: 'Referrer-Policy',
            value: 'strict-origin-when-cross-origin',
          },
          {
            key: 'Permissions-Policy',
            value: 'camera=(), microphone=(), geolocation=(), payment=()',
          },
        ],
      },
    ];
  },
};

export default nextConfig;

For CSP with nonces in Next.js, use middleware.ts as shown in the CSP section above, since next.config.ts headers cannot include per-request dynamic values.

Verifying Your Headers

After deployment, verify headers are correct:

curl -s -I https://yourapp.com | grep -iE "strict-transport|content-security|x-frame|x-content-type|referrer|permissions"

Online tools like securityheaders.com provide a graded report. ShipSafer's security headers scanner checks all of these automatically and flags missing or misconfigured values.

Common Mistakes to Avoid

  • CSP with unsafe-inline — negates XSS protection entirely. Use nonces.
  • HSTS without includeSubDomains when subdomains exist — attackers can strip HTTPS on subdomains and steal cookies.
  • Not setting always flag in nginx — without always, headers are only added to 200 responses, not 4xx/5xx pages.
  • Setting HSTS over HTTP — the browser ignores it, and you may confuse yourself thinking it is active.
  • Omitting Vary: Origin on dynamic CORS headers — can cause CDN cache poisoning.

Security headers require minimal ongoing maintenance once set correctly. Automated scanning ensures they do not regress during deployments.

Check Your Security Score — Free

See exactly how your domain scores on DMARC, TLS, HTTP headers, and 25+ other automated security checks in under 60 seconds.