Blog · 8 min · For any web app

The security headers your app is missing, and what each one actually does

Missing security headers is the finding we report more than any other. It is also the one people skip past, because a missing header does not feel like a breach. Here is what each header actually stops, and why we still flag it.

September 2026 · Zoltra · No account needed to use this fix

A missing security header is a browser instruction your server forgot to send. Without it, the browser guesses, and browsers guess in ways that help attackers. None of these headers fix a bug in your code. They close off the cheap tricks that sit on top of one.

That distinction matters. If your app has an open API route, a strict Content-Security-Policy will not save you. But if a customer gets clickjacked into your admin panel, or a browser treats an uploaded file as script, or someone on public wifi quietly downgrades the connection, headers are the layer that was supposed to be standing there.

Why we report it at all

When Zoltra checks a public surface, it reads the response headers and compares them against the reviewed list. Anything missing becomes a finding like Missing HTTP security header: x-frame-options. Severity depends on the header. A missing permissions-policy is informational. A missing strict-transport-security on a login page is a real problem.

The reason this shows up on nearly every fresh deployment is not carelessness. Next.js and most frameworks send a small safe default set and stop. The rest of the headers are a deliberate decision each product never gets around to making. There is no error when you forget. The page loads fine. It just loads less safely than it could.

The headers worth actually adding

You do not need all of them on day one. These are the ones that carry their weight, in roughly the order we would add them.

Strict-Transport-Security (HSTS)

Tells the browser: never talk to this domain over plain HTTP again, for this many seconds. Without it, the first request of every session can be intercepted and downgraded before the redirect to HTTPS happens. Start with max-age=31536000 once you are certain every subdomain is HTTPS. We do not recommend preload until you are very sure. It is hard to undo.

X-Content-Type-Options: nosniff

Stops the browser from second-guessing your declared content type. The classic abuse: an uploaded file declared as an image gets sniffed as HTML or JavaScript and runs. One line, almost no downside. Add it.

X-Frame-Options / frame-ancestors

Stops other sites from loading your app in an invisible frame and overlaying fake buttons. DENY if nothing frames you, SAMEORIGIN if your own pages do. The modern equivalent lives inside CSP as frame-ancestors, but sending both is fine and helps older browsers.

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

Limits how much of your URL leaks to other sites when someone clicks a link. Without a policy, full paths can travel in the Referer header, and paths sometimes contain tokens. This one is a quiet win with zero breakage risk.

Permissions-Policy

Turns off browser capabilities you do not use: camera, microphone, geolocation, payment. If you never call getUserMedia, say so. It is one less surface an embedded widget can ask for on your behalf.

Content-Security-Policy (CSP)

The big one, and the one most likely to break your app if you rush it. CSP tells the browser what scripts, styles, and frames are allowed to run. Done well, it is the strongest backstop against injected scripts you will get. Done badly in production, it breaks your analytics, your fonts, or the whole page.

Our advice: start with a policy in report-only mode (Content-Security-Policy-Report-Only), watch the violations for a week, then enforce. And read the header as a project, not a checkbox. If your app inlines scripts, enable nonces in middleware first, or you will have a long week.

The fix, for a Next.js app

Static headers belong in next.config.js. Here is a starting set that is safe for almost any app. Adjust the CSP once you know what your app loads.

// next.config.js
const securityHeaders = [
  { key: "Strict-Transport-Security", value: "max-age=31536000" },
  { key: "X-Content-Type-Options", value: "nosniff" },
  { key: "X-Frame-Options", value: "DENY" },
  { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
  { key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" },
];

/** @type {import('next').NextConfig} */
const nextConfig = {
  async headers() {
    return [{ source: "/(.*)", headers: securityHeaders }];
  },
};

module.exports = nextConfig;

CSP is the exception. If you need per-request nonces, generate it in middleware.ts and set the header there. A one-line default-src 'self' in the config is better than nothing, but it will break inline styles in many setups, so ship it report-only first.

How to verify it actually landed

One command. The -I flag asks for headers only:

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

# each one you configured should appear
# then check a real page, not just the homepage
curl -sI https://your-app.example/login | grep -i strict-transport

Check a route behind your middleware too. Static header config in Next.js applies at the platform layer, but a reverse proxy or CDN can strip headers before they reach the browser. What matters is what the browser receives, not what the config says.

How this drifts back

The usual pattern: the headers go in, everything is green, and eight months later a new deployment path appears. A second service on a subdomain. A rewrite rule that bypasses the config. A move from one host to another where nobody copied next.config.js headers over. The homepage keeps its headers while the new login subdomain has none.

This is exactly the kind of thing that is easy to check and easy to forget. Re-run the curl command against every hostname you own after any infrastructure change. Or let something check it for you, which is roughly what Zoltra does all day.

What headers do not fix

Headers are a seatbelt, not a repair. An open route is still open. A leaked key is still leaked. If a finding says your database is readable by anyone, adding nosniff does not change that. Fix the thing first, then add the headers that make the next mistake cheaper.

One more honest note: we do not flag every header on every app with the same weight. If you are building a weekend project with no logins and no data, the CSP conversation can wait. If people sign in, or money moves, or you store anything anyone would want, the list above is table stakes, and browsers have spent a decade making them cheap to send.

You can still ship this fix yourself. With GitHub connected, Zoltra can also prepare the repair as a pull request, leave it for your review, and verify the live result after deployment.


Related: Lock every table with RLS and Close your open API routes — the two fixes that matter more than any header.