Skip to content
All guides

Missing security headers: what to add and how (Next.js, Vercel, nginx)

By Obscura Security · Updated · Next.js, Vercel, nginx

Short answer

Add Strict-Transport-Security, X-Content-Type-Options: nosniff, Referrer-Policy, Permissions-Policy and a Content-Security-Policy with frame-ancestors to every response, using next.config.js headers(), vercel.json or nginx add_header. They limit the damage when something else goes wrong. They don't fix bugs in your code.

Security headers are lines your server adds to each response that tell the browser to behave more strictly. They take a few minutes to add. AI-built apps often have none of them.

Be clear about what they do: they limit the damage from some kinds of attack. They don’t repair a missing database rule, an exposed key or a broken login. Treat them as a second layer.

What each header does

  • Content-Security-Policy (CSP): lists where scripts, styles, images and frames may load from. If an attacker manages to inject a script into your page, a good policy can stop it running or phoning home.
  • Strict-Transport-Security (HSTS): tells the browser to use only HTTPS for your domain, which blocks downgrade attacks on later visits.
  • X-Content-Type-Options: nosniff: stops the browser guessing a file’s type, so an uploaded file can’t be run as a script by being mislabelled.
  • frame-ancestors (in CSP) or X-Frame-Options: controls which sites may embed yours in an iframe, which stops clickjacking.
  • Referrer-Policy: controls how much of your page’s URL is sent to other sites when users click links or load resources.
  • Permissions-Policy: switches off browser features you don’t use, such as camera, microphone and geolocation.

How to check your own site

  1. Open your site, open the browser’s developer tools, go to the Network tab, reload, click the first request (your page), and read the Response Headers.
  2. Or run this from a terminal:
curl -I https://YOUR-SITE.com
  1. Free online scanners such as securityheaders.com grade the same headers. Only scan sites you own.

Your host may already add some headers (some add HSTS automatically), so check what you actually get before adding duplicates.

A sensible starter set

Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'none'; base-uri 'self'; object-src 'none'; form-action 'self'

Notes on this set:

  • includeSubDomains applies HSTS to every subdomain, so use it only if all of them work over HTTPS. Don’t add preload unless you’ve read what submitting to the preload list commits you to, because it is hard to undo.
  • form-action 'self' blocks forms that submit to another site. Remove it if you have any.
  • Remove camera=() and the others for any feature your app does use.
  • Replace X-Frame-Options: DENY and frame-ancestors 'none' with SAMEORIGIN and 'self' if your own pages embed each other.
  • This CSP deliberately has no script-src, so it won’t break your scripts. It blocks framing, plugins and injected <base> tags, and it limits where forms can submit. A full policy that also restricts scripts is the bigger gain but needs testing (see below).

Add them in Next.js

// next.config.js
const securityHeaders = [
  { key: "Strict-Transport-Security", value: "max-age=31536000; includeSubDomains" },
  { key: "X-Content-Type-Options", value: "nosniff" },
  { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
  { key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" },
  { key: "X-Frame-Options", value: "DENY" },
  {
    key: "Content-Security-Policy",
    value: "frame-ancestors 'none'; base-uri 'self'; object-src 'none'; form-action 'self'",
  },
];

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

Add them on Vercel

For a non-Next.js project, or to set them outside the framework, use vercel.json:

{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        { "key": "Strict-Transport-Security", "value": "max-age=31536000; includeSubDomains" },
        { "key": "X-Content-Type-Options", "value": "nosniff" },
        { "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
        { "key": "Permissions-Policy", "value": "camera=(), microphone=(), geolocation=()" },
        { "key": "X-Frame-Options", "value": "DENY" },
        {
          "key": "Content-Security-Policy",
          "value": "frame-ancestors 'none'; base-uri 'self'; object-src 'none'; form-action 'self'"
        }
      ]
    }
  ]
}

Add them in nginx

Put these in the server block, then run sudo nginx -t && sudo systemctl reload nginx:

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header X-Frame-Options "DENY" always;
add_header Content-Security-Policy "frame-ancestors 'none'; base-uri 'self'; object-src 'none'; form-action 'self'" always;

nginx only inherits add_header lines from an outer block if the inner block has none of its own. If a location block adds even one header, repeat all of them there. Include always so the headers also appear on error responses.

Tightening the CSP

To restrict scripts as well, add default-src 'self' and a script-src, listing every place your page loads from (analytics, Stripe, fonts and so on). Do it in Content-Security-Policy-Report-Only first, load every page of your app, and read the browser console for violations before switching to the enforcing header. Next.js apps with inline scripts need nonces to do this; follow the framework’s own CSP guide.

Mistakes to avoid

  • Copying a strict CSP from a blog and shipping it untested. It can silently break login, payments or analytics.
  • Using 'unsafe-inline' and 'unsafe-eval' in script-src and calling it done. That allows exactly what CSP exists to block.
  • Setting the header in one place and overriding it in another. Check the live response, not the config file.
  • Expecting headers to fix a leak. They have no effect on an exposed database, a public .env or a key in your frontend.

Common questions

Do security headers make my app secure?
No. They are a second layer. A strict Content-Security-Policy can limit what an injected script does, but it won't fix a database that anyone can read or an admin route with no check. Fix the bugs first and add headers on top.
Will a Content-Security-Policy break my site?
A strict one can, because it blocks scripts, styles and images from sources you haven't listed. Start with the Content-Security-Policy-Report-Only header, which reports problems without blocking anything, or with a policy that only restricts framing and base URLs.
Do I still need X-Frame-Options if I use frame-ancestors?
Modern browsers use the CSP frame-ancestors directive when both are present. Many people keep X-Frame-Options as well for older browsers, and it does no harm.
What about X-XSS-Protection?
Leave it out. Modern browsers have removed the feature it controlled, and a strong CSP does the job properly.

Written by Obscura Security, an Australian practice that checks web apps for problems like this one. If you want a second pair of eyes on yours, email [email protected].