Security Headers Best Practices: CSP, HSTS, X-Frame-Options

TL;DR

The #1 security headers best practice is to add defense-in-depth headers that protect against XSS, clickjacking, and protocol downgrade attacks. Start with X-Content-Type-Options, X-Frame-Options, and Strict-Transport-Security. Add Content-Security-Policy for best protection. Test with securityheaders.com.

"Security headers are your first line of defense. They cost nothing to implement but protect against entire classes of attacks."

Essential Security Headers

Every web app should ship these headers:

Essential headers (copy-paste ready)
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Strict-Transport-Security: max-age=31536000; includeSubDomains

Header-by-Header Explanation

Strict-Transport-Security (HSTS) 2 min

Forces browsers to use HTTPS, preventing protocol downgrade attacks:

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

// Options:
// max-age: Time in seconds to remember HTTPS-only (1 year recommended)
// includeSubDomains: Apply to all subdomains
// preload: Submit to browser preload list (permanent, use carefully)

HSTS is sticky. Once set, browsers will refuse HTTP connections for the max-age period. We've seen people skip the short test window and get locked into a year of HSTS on a misconfigured domain. Start with a short max-age (3600) to test, then increase to 31536000 (1 year).

Content-Security-Policy (CSP) 5 min

The most powerful header for preventing XSS:

Basic CSP
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://api.yourdomain.com; frame-ancestors 'none'
DirectiveControlsRecommended Value
default-srcFallback for all resource types'self'
script-srcJavaScript sources'self' (avoid 'unsafe-inline')
style-srcCSS sources'self' 'unsafe-inline'
img-srcImage sources'self' data: https:
connect-srcFetch, XHR, WebSocket'self' your-api
frame-ancestorsWho can embed you'none' or 'self'

X-Frame-Options 1 min

Stops clickjacking by controlling whether your site can be embedded elsewhere:

X-Frame-Options values
X-Frame-Options: DENY           // Cannot be embedded anywhere
X-Frame-Options: SAMEORIGIN     // Only same origin can embed
X-Frame-Options: ALLOW-FROM uri // Deprecated, use CSP instead

X-Content-Type-Options 1 min

Prevents MIME type sniffing attacks:

MIME sniffing prevention
X-Content-Type-Options: nosniff

// Without this, browsers might execute a file as JavaScript
// even if served with a different Content-Type

Referrer-Policy 1 min

Controls what referrer information is sent with requests:

Referrer-Policy options
Referrer-Policy: no-referrer                    // Never send referrer
Referrer-Policy: strict-origin-when-cross-origin // Recommended
Referrer-Policy: same-origin                     // Only for same-origin

Permissions-Policy 1 min

Controls which browser features your site is allowed to touch:

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

// Disable features you don't use to reduce attack surface
// () = disabled, (self) = this origin only, (*) = any origin

Framework Configuration

Next.js 2 min

next.config.js
module.exports = {
  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=()' },
        ],
      },
    ];
  },
};

Express 2 min

Express with Helmet
import helmet from 'helmet';

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", "data:", "https:"],
    },
  },
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
  },
}));

Vercel/Netlify 1 min

vercel.json
{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        { "key": "X-Content-Type-Options", "value": "nosniff" },
        { "key": "X-Frame-Options", "value": "DENY" },
        { "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" }
      ]
    }
  ]
}

Testing Your Headers

Use these tools to verify your configuration:

  • securityheaders.com: Comprehensive header analysis
  • Mozilla Observatory: Full security audit
  • Browser DevTools: Network tab shows response headers
  • curl -I: Quick header check from terminal

Learn More: For comprehensive documentation on security headers, see MDN Web Docs: HTTP Security Headers and OWASP Secure Headers Project.

Do I need both X-Frame-Options and frame-ancestors?

For maximum compatibility, use both. CSP frame-ancestors is more flexible and overrides X-Frame-Options in modern browsers, but X-Frame-Options still covers older ones.

Why does CSP break my site?

CSP blocks resources that don't match your policy. Start with report-only mode (Content-Security-Policy-Report-Only) to find violations without breaking anything, then fix and switch to enforcing mode.

Should I use X-XSS-Protection?

Modern browsers have deprecated it, and it can actually introduce vulnerabilities in older ones. Include it for legacy support, but lean on CSP for real protection.

Further Reading

Put these practices into action with our step-by-step guides.

Check Your Security Headers

Scan your site for missing or misconfigured security headers.

Best Practices

Security Headers Best Practices: CSP, HSTS, X-Frame-Options