NC Logo UseToolSuite
Web Security

HTTP Security Headers: The Complete Checklist for Your Web App

A practical guide to HTTP security headers — CSP, HSTS, X-Frame-Options, and more. Learn what each header does, how to configure it, and the real attacks they prevent.

Necmeddin Cunedioglu Necmeddin Cunedioglu

Practice what you learn

HTTP Header Analyzer

Try it free →

A security audit flagged one of our production apps last year. The app itself was solid — input validation, parameterized queries, proper authentication. But the auditor ran a header check and came back with a list: no Content-Security-Policy, no HSTS, no X-Content-Type-Options. The app was serving responses with basically zero browser-side protection. We fixed it in an afternoon, but it was embarrassing that we’d shipped to production without these basics.

HTTP security headers are the easiest security win you’ll ever get. Most take a single line to add, they protect against real attack vectors, and they work across all modern browsers. Here’s what you need and why.

Why Headers Matter

Your server sends HTTP response headers with every page. Security headers instruct the browser on how to behave — what to load, what to block, whether to upgrade connections. Without them, the browser uses its most permissive defaults, leaving your users exposed to:

  • Cross-site scripting (XSS) — malicious scripts executing in your page context
  • Clickjacking — your page embedded in a hidden iframe to steal clicks
  • Protocol downgrade attacks — forcing HTTPS connections back to HTTP
  • MIME type confusion — the browser interpreting uploaded files as executable scripts

Check your headers: Paste your site’s response headers into our HTTP Header Analyzer to get an instant security score with specific recommendations for each missing or misconfigured header.

Content-Security-Policy (CSP)

CSP is the most powerful security header — and the most complex to configure correctly. It tells the browser exactly which resources are allowed to load and execute on your page.

The Basics

Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'

Each directive controls a resource type:

DirectiveControlsExample
default-srcFallback for all resource types'self'
script-srcJavaScript files and inline scripts'self' https://cdn.example.com
style-srcCSS files and inline styles'self' 'unsafe-inline'
img-srcImages'self' data: https:
connect-srcFetch, XHR, WebSocket destinations'self' https://api.example.com
frame-srcSources for iframes'none'
font-srcWeb fonts'self' https://fonts.gstatic.com

Starting with CSP: The Report-Only Approach

Don’t deploy a strict CSP in production on day one — you’ll break things. Use Content-Security-Policy-Report-Only first:

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

This logs violations without blocking anything. Review the reports, adjust the policy, and only then switch to enforcement.

Common CSP Mistakes

Mistake 1: Using unsafe-inline and unsafe-eval everywhere

# This CSP is almost useless — it allows inline scripts and eval()
Content-Security-Policy: script-src 'self' 'unsafe-inline' 'unsafe-eval'

These directives defeat the main purpose of CSP (blocking injected scripts). Instead, use nonces or hashes:

<!-- Server generates a random nonce per request -->
Content-Security-Policy: script-src 'nonce-abc123'

<script nonce="abc123">
  // This script runs because the nonce matches
</script>

<script>
  // This script is blocked — no matching nonce
  alert('XSS attempt');
</script>

Mistake 2: Forgetting connect-src for API calls

Your frontend makes fetch/XHR requests to your API. Without connect-src, the browser may block these requests — and the error looks confusingly similar to a CORS failure.

Mistake 3: Not including frame-ancestors

frame-ancestors controls who can embed your page in an iframe. It’s the modern replacement for X-Frame-Options:

Content-Security-Policy: frame-ancestors 'self'

Strict-Transport-Security (HSTS)

HSTS tells the browser: “Always use HTTPS for this domain, even if the user types http://.”

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
ParameterMeaning
max-ageHow long (seconds) the browser remembers to use HTTPS
includeSubDomainsApply HSTS to all subdomains too
preloadEligible for the HSTS preload list (hardcoded in browsers)

Why HSTS Matters

Without HSTS, even if your server redirects HTTP to HTTPS, the first request is still unencrypted. An attacker on the same network (coffee shop Wi-Fi, anyone?) can intercept that first request and perform a “SSL stripping” attack — downgrading the connection to HTTP permanently.

The Deployment Order

  1. Start with a short max-age (300 seconds) to test
  2. Verify everything works over HTTPS, including subdomains
  3. Increase to max-age=31536000 (1 year)
  4. Add includeSubDomains
  5. Submit to the HSTS preload list — this gets your domain hardcoded into browsers

Warning: Once you’re on the preload list, removing HSTS is very difficult. Make sure every subdomain supports HTTPS first.

X-Content-Type-Options

X-Content-Type-Options: nosniff

This single header prevents “MIME type sniffing” — where the browser ignores the Content-Type header and guesses the file type from the content. An attacker could upload a file named avatar.jpg containing JavaScript, and a browser doing MIME sniffing might execute it as a script.

There’s only one valid value (nosniff), and there’s no reason not to set it on every response.

X-Frame-Options

Controls whether your page can be loaded in an iframe:

X-Frame-Options: DENY          # Never allow framing
X-Frame-Options: SAMEORIGIN    # Only allow framing by same origin

This prevents clickjacking — where an attacker embeds your page in a transparent iframe and tricks users into clicking buttons they can’t see.

Note: X-Frame-Options is being superseded by CSP’s frame-ancestors directive, but include both for compatibility with older browsers:

Content-Security-Policy: frame-ancestors 'self'
X-Frame-Options: SAMEORIGIN

Referrer-Policy

Controls how much referrer information is sent when users navigate away from your site:

Referrer-Policy: strict-origin-when-cross-origin
ValueBehavior
no-referrerNever send referrer
originSend only the origin (no path)
strict-origin-when-cross-originFull URL for same-origin, origin only for cross-origin, nothing for downgrade
same-originFull URL for same-origin only

Recommended: strict-origin-when-cross-origin — it’s the browser default and a good balance between privacy and functionality.

Why this matters: Without a referrer policy, your full URL (including query parameters) leaks to every external link. If your URLs contain tokens, user IDs, or search queries, that’s sensitive data leaking to third parties.

Permissions-Policy (formerly Feature-Policy)

Controls which browser features your site can use:

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

The () means “nobody” — not even your own page. This prevents third-party scripts (analytics, ads) from accessing sensitive APIs:

Permissions-Policy: camera=(self), microphone=(), geolocation=(self "https://maps.example.com")

Implementation: Framework Examples

Express.js with Helmet

const helmet = require('helmet');

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

Nginx

server {
    # HSTS
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

    # CSP
    add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;" always;

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

    # Clickjacking protection
    add_header X-Frame-Options "SAMEORIGIN" always;

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

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

Next.js

// next.config.js
const securityHeaders = [
  { key: 'Strict-Transport-Security', value: 'max-age=31536000; includeSubDomains; preload' },
  { key: 'X-Content-Type-Options', value: 'nosniff' },
  { key: 'X-Frame-Options', value: 'SAMEORIGIN' },
  { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
];

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

Django

# settings.py
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'SAMEORIGIN'
SECURE_REFERRER_POLICY = 'strict-origin-when-cross-origin'

# CSP with django-csp
CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'",)
CSP_STYLE_SRC = ("'self'", "'unsafe-inline'")
CSP_IMG_SRC = ("'self'", "data:", "https:")

The Minimum Viable Security Headers

If you do nothing else, add these five headers to every response:

Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Referrer-Policy: strict-origin-when-cross-origin
Content-Security-Policy: default-src 'self'

That’s five lines that protect against XSS, clickjacking, MIME sniffing, protocol downgrades, and referrer leakage. It takes five minutes to implement and defends against attack vectors that have been exploited in the wild for decades.

Further Reading


Want to check your site’s security headers? Paste your HTTP response headers into our HTTP Header Analyzer for an instant score and specific fix recommendations. Pair it with the SSL Certificate Checker for a complete security posture review.

Necmeddin Cunedioglu
Necmeddin Cunedioglu Author

Software developer and the creator of UseToolSuite. I write about the tools and techniques I use daily as a developer — practical guides based on real experience, not theory.