Back to all posts
10 minGuidesApril 23, 2026

Security Headers Every Web App Needs: A Complete Implementation Guide

Security headers are the easiest high-impact security improvement you can make. This guide covers every header that matters, what it does, how to implement it on any platform, and common mistakes to avoid.

RM

Ryan Macomber

Founder, VibeSec Advisory

The easiest security win you are not using

HTTP security headers are instructions your server sends to the browser that control how your content is handled. They block clickjacking, prevent XSS exploitation, stop MIME-type confusion attacks, and enforce encrypted connections. They are a defense layer that works even when your application code has vulnerabilities.

Adding security headers takes 15-30 minutes. The protection they provide is disproportionately high for the effort involved. Yet most applications, especially those built with AI coding tools, ship without any security headers configured.

This guide covers every header that matters, explains what each one does in plain language, shows you how to implement them on the most common platforms, and warns you about the mistakes that break things.

The essential headers

Content-Security-Policy (CSP)

What it does: Tells the browser exactly which sources are allowed to load scripts, styles, images, fonts, and other resources on your page. Anything not on the allow-list is blocked.

Why it matters: CSP is the most powerful defense against Cross-Site Scripting (XSS). Even if an attacker manages to inject a <script> tag into your page, the browser will refuse to execute it because the script's source is not on your CSP allow-list.

Implementation:

Start with a restrictive policy and add exceptions as needed:

Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self'; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; object-src 'none'; base-uri 'self'; form-action 'self'; upgrade-insecure-requests

Breaking this down:

| Directive | Value | Meaning | | --------------------------- | ------------------------ | --------------------------------------------------------------------------- | | default-src | 'self' | By default, only load resources from your own domain | | script-src | 'self' | Only run scripts from your domain (no inline scripts, no CDNs unless added) | | style-src | 'self' 'unsafe-inline' | Styles from your domain plus inline styles (needed for most CSS-in-JS) | | font-src | 'self' | Fonts from your domain only (add Google Fonts URL if needed) | | img-src | 'self' data: | Images from your domain and data URIs (for inline SVGs, etc.) | | connect-src | 'self' | API calls only to your domain (add external API domains as needed) | | frame-ancestors | 'none' | Your page cannot be embedded in an iframe (prevents clickjacking) | | object-src | 'none' | No Flash or Java applets (legacy but still important to block) | | base-uri | 'self' | Prevents attackers from changing the base URL for relative links | | form-action | 'self' | Forms can only submit to your domain | | upgrade-insecure-requests | (flag) | Automatically upgrade HTTP requests to HTTPS |

Common additions:

If you use Stripe:

script-src 'self' https://js.stripe.com;
frame-src https://js.stripe.com https://checkout.stripe.com;
connect-src 'self' https://api.stripe.com;

If you use Google Fonts:

style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
font-src 'self' https://fonts.gstatic.com;

If you use Google Analytics:

script-src 'self' https://www.googletagmanager.com;
connect-src 'self' https://www.google-analytics.com;
img-src 'self' data: https://www.google-analytics.com;

Common mistakes:

  • Using script-src 'unsafe-inline' 'unsafe-eval' defeats the purpose of CSP entirely
  • Setting CSP as Content-Security-Policy-Report-Only and forgetting to switch to enforcement
  • Using * in any directive (allows everything, provides no protection)

Testing: After deploying CSP, open your browser's developer console. CSP violations appear as errors. Fix each violation by adding the necessary source to the appropriate directive, or by refactoring the code to avoid the violation.

Strict-Transport-Security (HSTS)

What it does: Tells the browser to always use HTTPS for your domain, even if the user types http:// or clicks an HTTP link.

Why it matters: Without HSTS, the first request to your site might be over HTTP before the redirect to HTTPS kicks in. An attacker on the same network (coffee shop Wi-Fi, for example) can intercept that first HTTP request and serve a fake version of your site (SSL stripping attack).

Implementation:

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

| Parameter | Meaning | | ------------------- | ------------------------------------------------------------------------------------- | | max-age=31536000 | Remember this setting for 1 year (in seconds) | | includeSubDomains | Apply to all subdomains too | | preload | Eligible for browser preload lists (Chrome, Firefox ship with a list of HSTS domains) |

Common mistakes:

  • Setting max-age to a short value (use at least 31536000)
  • Forgetting includeSubDomains (attackers can use http://sub.yourdomain.com to bypass)
  • Enabling HSTS before ensuring all subdomains support HTTPS

X-Frame-Options

What it does: Controls whether your page can be embedded in an <iframe>.

Why it matters: Without this, an attacker can create a page that loads your site in a hidden iframe and tricks users into clicking buttons they cannot see (clickjacking). This can lead to account deletion, money transfers, or other destructive actions.

Implementation:

X-Frame-Options: DENY

Options:

  • DENY — page cannot be framed by any site (recommended for most apps)
  • SAMEORIGIN — page can only be framed by your own domain

Note: frame-ancestors 'none' in CSP does the same thing and is the modern replacement. Use both for backward compatibility.

X-Content-Type-Options

What it does: Prevents the browser from guessing ("sniffing") the content type of a response. The browser must use the Content-Type header you set.

Why it matters: Without this, a browser might interpret a JSON response or uploaded file as HTML and execute JavaScript within it. An attacker uploads a file with a .jpg extension but HTML content, and the browser executes it as a web page.

Implementation:

X-Content-Type-Options: nosniff

This is a single, simple value. Always set it.

Referrer-Policy

What it does: Controls how much URL information is sent in the Referer header when users navigate from your site to other sites.

Why it matters: URLs sometimes contain sensitive information: session tokens in query strings, internal page paths, search queries. The Referer header leaks this to every site your users navigate to.

Implementation:

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

This sends the full URL for same-origin requests (your own site) but only the origin (domain) for cross-origin requests. It is the best balance of functionality and privacy.

Stricter option: no-referrer sends no referrer at all, but this breaks some analytics and affiliate tracking.

Ready to apply the FORGE framework?

VibeSec helps knowledge worker teams redesign their processes using the FORGE framework: Skills, Agents, Guardrails, and Schedule. Security is built in, not bolted on. Map your first process in 10 minutes.

Permissions-Policy

What it does: Disables browser features your application does not use, like geolocation, microphone, camera, and payment APIs.

Why it matters: If an XSS vulnerability allows an attacker to run JavaScript on your page, they could access the user's camera, microphone, or location. Permissions-Policy blocks these APIs entirely if your app does not need them.

Implementation:

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

Each () means "disabled for all origins." If your app needs geolocation, for example, use geolocation=(self) to allow it only from your origin.

Cross-Origin-Opener-Policy (COOP)

What it does: Isolates your page from other windows/tabs that your page might have opened or been opened by.

Why it matters: This mitigates Spectre-style side-channel attacks that can read data from cross-origin windows. It also prevents other windows from getting a reference to your window object.

Implementation:

Cross-Origin-Opener-Policy: same-origin

Cross-Origin-Resource-Policy (CORP)

What it does: Controls which origins can include your resources (images, scripts, etc.) in their pages.

Why it matters: Prevents other sites from embedding your resources, which can be used for timing attacks, data exfiltration, or simply hotlinking your assets.

Implementation:

Cross-Origin-Resource-Policy: same-origin

Use same-site if you need resources to be accessible across subdomains. Use cross-origin only for intentionally public resources like CDN assets.

Platform-specific implementation

Cloudflare Pages

Create a public/_headers file:

/*
  X-Frame-Options: DENY
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin
  Permissions-Policy: geolocation=(), microphone=(), camera=(), payment=(), usb=()
  Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
  Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self'; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; object-src 'none'; base-uri 'self'; form-action 'self'; upgrade-insecure-requests
  Cross-Origin-Opener-Policy: same-origin
  Cross-Origin-Resource-Policy: same-origin

Or use a Cloudflare Pages Function middleware for dynamic header control (recommended for CSP that varies by route).

Vercel

In vercel.json:

{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        { "key": "X-Frame-Options", "value": "DENY" },
        { "key": "X-Content-Type-Options", "value": "nosniff" },
        {
          "key": "Referrer-Policy",
          "value": "strict-origin-when-cross-origin"
        },
        {
          "key": "Strict-Transport-Security",
          "value": "max-age=31536000; includeSubDomains; preload"
        },
        {
          "key": "Content-Security-Policy",
          "value": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; object-src 'none'; base-uri 'self'"
        }
      ]
    }
  ]
}

Netlify

In public/_headers:

/*
  X-Frame-Options: DENY
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin
  Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
  Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; object-src 'none'; base-uri 'self'

Express / Node.js

Using the helmet package (recommended):

npm install helmet
import helmet from "helmet";

app.use(
  helmet({
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'"],
        styleSrc: ["'self'", "'unsafe-inline'"],
        imgSrc: ["'self'", "data:"],
        connectSrc: ["'self'"],
        frameAncestors: ["'none'"],
        objectSrc: ["'none'"],
        baseUri: ["'self'"],
      },
    },
  })
);

Or manually:

app.use((req, res, next) => {
  res.setHeader("X-Frame-Options", "DENY");
  res.setHeader("X-Content-Type-Options", "nosniff");
  res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
  res.setHeader(
    "Strict-Transport-Security",
    "max-age=31536000; includeSubDomains; preload"
  );
  res.setHeader(
    "Content-Security-Policy",
    "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; object-src 'none'; base-uri 'self'"
  );
  next();
});

How to verify your headers

Command line

curl -I https://yourdomain.com

Check that each security header appears in the response.

Online tools

Browser DevTools

Open DevTools (F12) → Network tab → click on the main document request → check the Response Headers section. CSP violations appear in the Console tab as errors.

Common pitfalls

CSP breaks your app

The most common issue. You deploy a strict CSP and your app stops working because a third-party script, font, or API call is blocked.

Fix: Deploy with Content-Security-Policy-Report-Only first. This logs violations without blocking them. Review the violations in your browser console, add legitimate sources to your policy, then switch to enforcement mode.

Inline scripts and styles

CSP's script-src 'self' blocks inline <script> tags and onclick handlers. Many frameworks use inline styles, which are blocked by style-src 'self'.

Fix for styles: Add 'unsafe-inline' to style-src. This is acceptable because inline style injection is much less dangerous than inline script injection.

Fix for scripts: Refactor inline scripts to external files, or use CSP nonces:

Content-Security-Policy: script-src 'nonce-abc123'
<script nonce="abc123">
  /* allowed */
</script>

The nonce must be unique per request and cryptographically random.

HSTS on a site without full HTTPS

If any page, subdomain, or asset is served over HTTP, HSTS with includeSubDomains will break it. Ensure everything is HTTPS before enabling HSTS.

Overly permissive headers

Setting headers but making them too permissive:

  • Content-Security-Policy: default-src * — allows everything, provides zero protection
  • X-Frame-Options: ALLOW-FROM * — not a valid value (only DENY, SAMEORIGIN, or ALLOW-FROM specific-origin)
  • Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true — browsers reject this combination, but it signals a misunderstanding of CORS

Frequently Asked Questions

What security headers does every web application need?

At minimum, every web application should have these 6 headers: Content-Security-Policy (prevents XSS), Strict-Transport-Security (forces HTTPS), X-Frame-Options (prevents clickjacking), X-Content-Type-Options (prevents MIME sniffing), Referrer-Policy (controls referrer leakage), and Permissions-Policy (restricts browser features). These take 15 minutes to configure and dramatically improve your security posture.

How do I check if my site has security headers?

Run curl -sI https://yoursite.com in your terminal to see all response headers. You can also use securityheaders.com for a visual scan and grade. If you are missing Content-Security-Policy or Strict-Transport-Security, those should be your first priorities.

Why does my AI coding assistant never add security headers?

AI coding tools optimize for functionality — making your application work. Security headers are a defense-in-depth layer that does not affect whether your application functions correctly, so AI tools consistently skip them. In our security reviews, AI-generated applications have zero security headers configured in the vast majority of cases.

What is Content-Security-Policy and why is it the most important header?

Content-Security-Policy (CSP) tells browsers which resources are allowed to load on your page. It is the single most impactful security header because it prevents cross-site scripting (XSS) attacks even when your application code has vulnerabilities. A basic CSP that blocks inline scripts and restricts sources to your own domain stops most XSS attacks.

Can security headers break my application?

Yes, if configured incorrectly. Content-Security-Policy is the most likely to cause issues because it blocks resources that do not match your policy. Use Content-Security-Policy-Report-Only mode first to identify violations without blocking anything, fix the violations, then switch to enforcement mode.

Do I need different headers for different frameworks?

The headers themselves are the same regardless of framework. What differs is how you configure them — Express uses the helmet package, Next.js uses next.config.js headers, Cloudflare uses _headers files, and so on. The implementation examples above cover the most common deployment platforms.


Headers that AI coding tools miss

In our security reviews, AI-generated applications consistently have zero security headers configured. The AI generates working server code that responds to requests, and security headers are not part of making requests work.

This is one of the easiest security improvements you can make to an AI-built application:

  1. Add the headers using the platform-specific examples above
  2. Test with Content-Security-Policy-Report-Only first
  3. Fix any violations
  4. Switch to enforcement mode
  5. Verify with securityheaders.com

Fifteen minutes of work. Massive security improvement. Do it today.


Need help configuring security headers for your specific stack, or want a comprehensive review that goes beyond headers? Contact us. We help teams building with agentic AI ship secure applications.

Weekly security tips

Actionable security insights for vibe coders, delivered every Thursday. No spam, unsubscribe anytime.

By subscribing, you agree to receive marketing emails from VibeSec Advisory. You can unsubscribe at any time. Privacy Policy

Ready to apply the FORGE framework to your team?

Map your first process in 10 minutes and get deliverables within 48 hours. No call required.