VibeSecurity

Guide

Security headers on Vercel and Netlify: setup and verification

Security headers are instructions your host sends to the browser. They will not fix a leaked key or an open database, but they limit the damage of script injection, clickjacking and downgrade attacks, and AI-generated projects rarely set them. In this guide you will add a safe baseline to a Vercel or Netlify app, introduce a Content-Security-Policy in report-only mode, and verify with curl what your site really sends.

By the VibeSecurity team6 min read

What you need and what you will have at the end

You need a deployed app on Vercel or Netlify, write access to its repository, and curl in a terminal. Test only sites you own.

At the end your production site will send a baseline set of security headers on every response, you will have a Content-Security-Policy running in report-only mode with a plan to enforce it, and you will have a command that checks all of it from outside.

Step 1: Know which headers you are adding and why

  • Strict-Transport-Security tells browsers to use HTTPS only for your domain, and MDN documents max-age in seconds plus optional includeSubDomains and preload.
  • Content-Security-Policy limits where scripts, styles and frames can load from, which reduces the impact of an injected script.
  • X-Content-Type-Options: nosniff stops browsers guessing file types.
  • Referrer-Policy limits what part of your address other sites see when a visitor follows a link. OWASP recommends strict-origin-when-cross-origin.
  • frame-ancestors in a CSP stops other sites embedding your app in a frame, which is the defense against clickjacking. OWASP recommends it over the older X-Frame-Options header for modern browsers.
  • Permissions-Policy turns off browser features your app does not use, such as camera and microphone.

Step 2: Check what you send today

Request your production URL and read the response headers before changing anything, so you can prove afterward that the deploy did what you intended. Most AI-generated projects will show none of the six headers above.

Terminal
curl -sI https://your-app.example | grep -iE "strict-transport|content-security|x-content-type|referrer-policy|permissions-policy|x-frame"

Step 3a: Add the baseline on Vercel

Vercel lets you add custom response headers in the headers property of vercel.json, at your project root. Each entry has a source path pattern and a list of key and value pairs. The pattern below matches every path.

Commit the file and deploy. Note that a project can only use one of vercel.json, vercel.toml or vercel.ts for configuration, so add the block to whichever you already have instead of creating a second file.

vercel.json
{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        { "key": "Strict-Transport-Security", "value": "max-age=63072000; includeSubDomains" },
        { "key": "X-Content-Type-Options", "value": "nosniff" },
        { "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
        { "key": "Permissions-Policy", "value": "geolocation=(), camera=(), microphone=()" },
        { "key": "Content-Security-Policy", "value": "frame-ancestors 'none'" }
      ]
    }
  ]
}

Step 3b: Add the baseline on Netlify

Netlify reads a plain text file named _headers from your publish directory. The path goes on its own line, and each header is indented beneath it. For a framework like Vite, put the file in the public folder so the build copies it into the output directory, and confirm it appears there after a build.

If you prefer configuration in netlify.toml, the equivalent is a headers block with a for path and a values table. Use one approach, not both, so you do not have to reason about which one wins.

_headers
/*
  Strict-Transport-Security: max-age=63072000; includeSubDomains
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin
  Permissions-Policy: geolocation=(), camera=(), microphone=()
  Content-Security-Policy: frame-ancestors 'none'

Step 4: Roll out a full CSP in report-only mode

A complete Content-Security-Policy that restricts script-src can break analytics, payment widgets and inline scripts. MDN notes that a policy with default-src or script-src blocks inline scripts, inline event handlers and eval by default, and that nonces or hashes are the preferred way to allow specific inline code.

Send the policy as Content-Security-Policy-Report-Only first. The browser reports what would have been blocked without blocking it. Load every page and flow in your app, including sign-in and checkout, and open the browser console to see the reported violations. Add the specific sources you legitimately need, then enforce.

MDN describes report-to with the Reporting-Endpoints header as the modern reporting mechanism, and report-uri as the deprecated alternative. If you have no collector, the console messages are enough for a small app.

Report-only header
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'

Step 5: Enforce the CSP once the noise is gone

When a full pass through your app produces no violations you did not expect, change the header name from Content-Security-Policy-Report-Only to Content-Security-Policy. Keep the same value. Deploy, then repeat the walk through your app, because enforcing turns every leftover violation into a broken feature.

If you rely on third-party scripts, list each origin explicitly in script-src rather than loosening to a wildcard. Avoid unsafe-inline, which MDN describes as discouraged. Note that report-only mode cannot be set with a meta element, so use a real response header as above.

Step 6: Verify what you actually serve

After deploying, request your site again and read the headers. Check the home page and at least one deep route and one static asset, because path rules can match differently. Then repeat once more after every deploy for a few releases.

Terminal
for path in / /login /assets/app.js; do
  echo "== $path"
  curl -sI "https://your-app.example$path" | grep -iE "strict-transport|content-security|x-content-type|referrer-policy|permissions-policy"
done

Common mistakes

  • Editing the config but not redeploying, then testing an old deployment.
  • Putting _headers in the repository root instead of the publish directory, so Netlify never sees it.
  • Enforcing a strict CSP without a report-only phase and breaking sign-in or payments.
  • Setting X-Frame-Options and a conflicting frame-ancestors, or allowing framing where you did not intend to. Decide whether any site should embed yours, then set one clear policy.
  • Adding includeSubDomains while a subdomain still serves plain HTTP.
  • Copying a header list from an unrelated site with sources that do not match your own third-party scripts.

How to verify

Pass conditions
HeaderExpected valuePass condition
Strict-Transport-Securitymax-age=63072000; includeSubDomainsPresent on HTTPS responses
X-Content-Type-OptionsnosniffPresent on every path you checked
Referrer-Policystrict-origin-when-cross-originPresent
Permissions-Policycamera, microphone and geolocation disabledPresent
Content-Security-Policyframe-ancestors 'none' at minimumPresent, and the site loads in a frame test only if you allow it
CSP report-only phaseNo unexpected violations in the consoleZero unexplained violations before enforcing

Keep it working

Add any new third-party script, font host, analytics tool or embed to your CSP in report-only mode first, then enforce. Re-run the Step 6 loop after each deploy and after switching hosts or adding a proxy in front of your site, since a CDN or proxy can add or strip headers. Keep the header configuration in your repository so it is reviewed like code.

Frequently asked questions

Will security headers stop my app being hacked?

They reduce specific risks, such as script injection, clickjacking and HTTP downgrade, but they do not fix broken access control, leaked keys or open databases. Treat them as a cheap layer added after the bigger problems are handled.

Why start CSP in report-only mode?

A strict policy blocks anything it does not list, including inline scripts and third-party widgets. Report-only mode shows you what would break without breaking it, so you can add the sources you need and enforce with confidence.

Should I use X-Frame-Options or frame-ancestors?

For modern browsers, OWASP recommends the CSP frame-ancestors directive, which gives the same clickjacking protection with more flexibility. Setting frame-ancestors 'none' blocks all framing. Allow specific origins only if another site must embed your app.

Do I need the HSTS preload directive?

Not to get started. Preload adds your domain to browser lists, requires a long max-age and includeSubDomains, and is hard to reverse. Begin with a normal HSTS header, confirm all subdomains work over HTTPS, and decide on preload later.

Sources

  1. 1.MDN: Content Security Policy
  2. 2.MDN: Strict-Transport-Security
  3. 3.OWASP HTTP Headers Cheat Sheet
  4. 4.Vercel docs: Project configuration
  5. 5.Netlify docs: Headers