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

Share

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 application should include 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. 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

Prevents clickjacking by controlling if your site can be embedded:

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 can use:

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 supports older browsers.

Why does CSP break my site?

CSP blocks resources that do not 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 introduce vulnerabilities in older browsers. Include it for legacy support but rely on CSP for real protection.

Check Your Security Headers

Scan your site for missing or misconfigured security headers.

Start Free Scan
Best Practices

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