How to Add Security Headers to Your Web App

Share
How-To Guide

How to Add Security Headers to Your Web App

Protect your app with essential HTTP security headers

TL;DR

TL;DR (20 minutes)

Add security headers to protect against clickjacking, XSS, MIME sniffing, and protocol downgrade attacks. Essential headers: X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Strict-Transport-Security, Referrer-Policy, and Content-Security-Policy. Configure via your web server, framework middleware, or hosting platform.

Prerequisites

  • Access to your web server configuration, framework code, or hosting platform dashboard
  • Your site served over HTTPS (required for HSTS)
  • Basic understanding of HTTP headers
  • A way to test headers (browser DevTools or online scanner)

Why Security Headers Matter

HTTP security headers instruct browsers how to handle your site's content. Without them, your app is vulnerable to:

  • Clickjacking - Attackers embed your site in an invisible iframe to hijack clicks
  • MIME sniffing attacks - Browsers misinterpret file types, potentially executing malicious content
  • XSS attacks - Malicious scripts run in your users' browsers stealing data
  • Protocol downgrade - Attackers force HTTP connections to intercept traffic
  • Information leakage - Sensitive URLs exposed via referrer headers

Essential Security Headers Reference

HeaderRecommended ValueProtection
X-Content-Type-OptionsnosniffPrevents MIME type sniffing
X-Frame-OptionsDENY or SAMEORIGINBlocks clickjacking via iframes
Strict-Transport-Securitymax-age=31536000; includeSubDomainsEnforces HTTPS connections
Referrer-Policystrict-origin-when-cross-originLimits referrer information leakage
Content-Security-Policydefault-src 'self'; ...Controls resource loading, prevents XSS
Permissions-Policycamera=(), microphone=(), geolocation=()Restricts browser feature access

Step-by-Step Implementation

1

Express.js with Helmet

The easiest way to add security headers in Express is using the Helmet middleware:

npm install helmet
const express = require('express');
const helmet = require('helmet');

const app = express();

// Add all security headers with sensible defaults
app.use(helmet());

// Or configure individually:
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "'unsafe-inline'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", "data:", "https:"],
      connectSrc: ["'self'", "https://api.example.com"],
      fontSrc: ["'self'", "https://fonts.gstatic.com"],
      objectSrc: ["'none'"],
      upgradeInsecureRequests: [],
    },
  },
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true,
  },
}));

app.listen(3000);
2

Next.js Configuration

Add headers in next.config.js:

/** @type {import('next').NextConfig} */
const nextConfig = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          {
            key: 'X-Content-Type-Options',
            value: 'nosniff',
          },
          {
            key: 'X-Frame-Options',
            value: 'DENY',
          },
          {
            key: 'X-XSS-Protection',
            value: '1; mode=block',
          },
          {
            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',
          },
          {
            key: 'Content-Security-Policy',
            value: "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https:; frame-ancestors 'none';",
          },
        ],
      },
    ];
  },
};

module.exports = nextConfig;
3

Next.js Middleware (Dynamic Headers)

For dynamic CSP with nonces, use middleware in middleware.ts:

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const nonce = Buffer.from(crypto.randomUUID()).toString('base64');

  const cspHeader = `
    default-src 'self';
    script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
    style-src 'self' 'nonce-${nonce}';
    img-src 'self' blob: data: https:;
    font-src 'self';
    object-src 'none';
    base-uri 'self';
    form-action 'self';
    frame-ancestors 'none';
    upgrade-insecure-requests;
  `.replace(/\s{2,}/g, ' ').trim();

  const response = NextResponse.next();

  response.headers.set('Content-Security-Policy', cspHeader);
  response.headers.set('X-Content-Type-Options', 'nosniff');
  response.headers.set('X-Frame-Options', 'DENY');
  response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
  response.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
  response.headers.set('X-Nonce', nonce);

  return response;
}

export const config = {
  matcher: [
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
  ],
};
4

nginx Configuration

Add to your nginx server block:

server {
    listen 443 ssl http2;
    server_name example.com;

    # Security Headers
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "DENY" always;
    add_header X-XSS-Protection "1; mode=block" 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; preload" always;
    add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; frame-ancestors 'none';" always;

    # ... rest of your config
}
5

Apache Configuration

Add to your .htaccess or virtual host config:

<IfModule mod_headers.c>
    Header always set X-Content-Type-Options "nosniff"
    Header always set X-Frame-Options "DENY"
    Header always set X-XSS-Protection "1; mode=block"
    Header always set Referrer-Policy "strict-origin-when-cross-origin"
    Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
    Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
    Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;"
</IfModule>
6

Cloudflare Configuration

Add via Transform Rules in Cloudflare Dashboard:

  1. Go to Rules > Transform Rules > Modify Response Header
  2. Create a new rule for all requests
  3. Add each header as a "Set static" operation

Or use Cloudflare Workers:

export default {
  async fetch(request, env) {
    const response = await fetch(request);
    const newResponse = new Response(response.body, response);

    newResponse.headers.set('X-Content-Type-Options', 'nosniff');
    newResponse.headers.set('X-Frame-Options', 'DENY');
    newResponse.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
    newResponse.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
    newResponse.headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
    newResponse.headers.set('Content-Security-Policy', "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;");

    return newResponse;
  },
};

Security Checklist Before Deployment

  • Ensure your site is fully served over HTTPS before enabling HSTS
  • Test Content-Security-Policy in report-only mode first
  • Verify X-Frame-Options doesn't break legitimate iframe embeds
  • Check that third-party scripts are allowed in your CSP
  • Start with a short HSTS max-age (e.g., 300) and increase gradually
  • Test on staging environment before production deployment

How to Verify It Worked

Method 1: Browser DevTools

  1. Open your site in Chrome or Firefox
  2. Press F12 to open DevTools
  3. Go to the Network tab
  4. Reload the page (Ctrl+R or Cmd+R)
  5. Click on the first request (your HTML document)
  6. Scroll down to Response Headers
  7. Verify all your security headers are present

Method 2: Online Scanner

Use securityheaders.com to scan your site:

  1. Enter your URL and click Scan
  2. Review your grade (aim for A or A+)
  3. Check which headers are missing or misconfigured

Method 3: Command Line

# Using curl to check headers
curl -I https://yoursite.com

# Expected output includes:
# x-content-type-options: nosniff
# x-frame-options: DENY
# strict-transport-security: max-age=31536000; includeSubDomains
# referrer-policy: strict-origin-when-cross-origin
# content-security-policy: default-src 'self'; ...

Common Errors and Troubleshooting

Headers not appearing

  • CDN caching: Your CDN may be caching responses without headers. Purge the cache or add headers at the CDN level.
  • Wrong config location: Ensure headers are configured for all routes, not just specific paths.
  • Server not restarted: nginx and Apache require restart/reload after config changes.

CSP blocking content

  • Inline scripts blocked: Add 'unsafe-inline' to script-src (less secure) or use nonces.
  • Third-party resources blocked: Add the domain to the appropriate directive (e.g., script-src 'self' https://cdn.example.com).
  • Images not loading: Add data: and https: to img-src for base64 and external images.

HSTS causing issues

  • Can't access HTTP version: This is expected behavior. If you need to disable HSTS, set max-age=0 and wait for the original max-age to expire.
  • Subdomain not HTTPS-ready: Remove includeSubDomains until all subdomains support HTTPS.

X-Frame-Options breaking embeds

  • Legitimate embeds blocked: Use SAMEORIGIN instead of DENY, or use CSP frame-ancestors for more control.
  • Need to allow specific domains: Use Content-Security-Policy: frame-ancestors 'self' https://trusted.com

Pro Tip

Use Content-Security-Policy-Report-Only header first to test your CSP without breaking anything. Set up a reporting endpoint to collect violations and refine your policy before enforcing it.

Frequently Asked Questions

What security headers should every website have?

Every website should have: X-Content-Type-Options: nosniff, X-Frame-Options: DENY or SAMEORIGIN, Strict-Transport-Security with max-age of at least 31536000, Referrer-Policy: strict-origin-when-cross-origin, and Content-Security-Policy. These protect against the most common web attacks.

Will adding security headers break my website?

Basic headers like X-Content-Type-Options, X-Frame-Options, and Referrer-Policy rarely cause issues. HSTS can cause problems if your site isn't fully HTTPS-ready. CSP is most likely to break functionality if misconfigured - always test in report-only mode first.

Do security headers work on static sites?

Yes, security headers work on static sites. Configure them through your hosting platform: vercel.json for Vercel, _headers file for Netlify, or through your CDN's configuration. Headers are sent by the server regardless of content type.

What is the difference between X-Frame-Options and CSP frame-ancestors?

Both prevent clickjacking. X-Frame-Options only supports DENY, SAMEORIGIN, or ALLOW-FROM (deprecated). CSP frame-ancestors supports multiple domains and wildcards. Use both for browser compatibility, but prefer frame-ancestors for new implementations.

Do I need all these headers if I use a framework like React or Next.js?

Yes. Frameworks help with some security aspects but don't add HTTP headers automatically. You must configure security headers separately through your hosting platform or server configuration.

Check Your Security Headers

Run a free scan to see which security headers your site is missing and get specific recommendations.

Start Free Scan
How-To Guides

How to Add Security Headers to Your Web App