NEXHUB
Application Security

The Security Headers Checklist

6 min read

In short

The security headers worth sending on every HTML response are Content-Security-Policy, Strict-Transport-Security, X-Content-Type-Options, Referrer-Policy and Permissions-Policy, plus X-Frame-Options for older browsers. Cookies need Secure, HttpOnly and SameSite. Remove X-XSS-Protection and any header disclosing your server version.

Real compromises rarely start with something exotic. They start with a cookie missing an attribute, an error page naming the framework and its version, or a backup file that was never meant to be reachable and has been for two years.

This layer is the cheapest security work available. Most of it is configuration, not code, and a lot of it can be fixed in an afternoon. It is also the layer that every future auditor, customer security questionnaire and procurement review will check before anything else — so closing it once removes it from all of them.

The headers, in priority order

HeaderValue to aim forWhat it stops
Content-Security-Policynonce + 'strict-dynamic' + default-src 'self'An injected script from doing anything useful
Strict-Transport-Securitymax-age=31536000; includeSubDomainsDowngrade to plaintext after the first visit
X-Content-Type-OptionsnosniffThe browser guessing a type and executing an upload
Referrer-Policystrict-origin-when-cross-originLeaking full URLs, including tokens, to third parties
Permissions-PolicyDeny what you do not useCamera, microphone, geolocation and payment access
X-Frame-OptionsDENYClickjacking in browsers predating CSP frame-ancestors
Cross-Origin-Opener-Policysame-originCross-window scripting attacks and some side channels

Content-Security-Policy

The most valuable and the most work, so it gets its own article: a Content-Security-Policy that actually works.

The short version. A policy without script-src and without default-src restricts nothing that matters, no matter how many other directives it carries. Use a per-request nonce with 'strict-dynamic', deploy in report-only mode first, and set frame-ancestors, base-uri, form-action and object-src explicitly — default-src does not cover those.

Strict-Transport-Security

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

HSTS, defined in RFC 6797, tells the browser to refuse plaintext HTTP to your host for the stated period. 31536000 seconds is one year and is the accepted value.

The catch is in the name of the mechanism: strict. Once a browser has seen this header it will not talk to your host over HTTP until the max-age expires, and you cannot revoke that remotely. Confirm every subdomain genuinely serves HTTPS before adding includeSubDomains. Adding preload is stronger again and considerably harder to undo — treat it as a decision, not a default.

X-Content-Type-Options

X-Content-Type-Options: nosniff

Without it, browsers may ignore your declared Content-Type and infer one from the bytes. That turns a user-uploaded file which merely looks like script into script.

One value, no configuration, no downside. Send it on API responses too, not just HTML — it is routinely missing there because people add headers at the page layer and forget the JSON.

Referrer-Policy

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

Controls how much of the current URL is sent when a user follows a link away from your site. The default behaviour on many setups sends the full path and query string to the destination.

That matters more than it sounds. Password reset links, invitation tokens and session identifiers all live in URLs on plenty of real applications. This value sends the origin only when crossing to another site, and nothing at all when downgrading to HTTP.

Permissions-Policy

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

The empty parentheses mean "nobody, including this document". Deny every capability you do not actively use. If an injection ever lands on your page, this is what stops it from quietly asking for the microphone.

Two headers to remove

X-XSS-Protection

X-XSS-Protection: 1; mode=block   ← delete this

This is still recommended by a surprising number of blog posts and still shipped by a surprising number of frameworks. The auditors are behind on it too, which is how it survives.

It controlled a browser XSS filter that no longer exists in any current browser. Worse, that filter introduced its own vulnerability class, where an attacker could use it to selectively break parts of a page. Remove the header. Content-Security-Policy is the replacement, and it is not close.

Anything naming your software version

Server: nginx/1.24.0 and X-Powered-By: PHP/8.1.2 hand an attacker your patch level for free. Naming the product without the version is a much smaller matter — Via: 1.1 Caddy is untidy rather than dangerous — but the version number should go.

While you are there, check for internal routing headers. Frameworks sometimes emit things like an X-Pathname echoing the resolved internal route. Harmless in development, and it should not be leaving your edge in production.

Cookies: the part everybody forgets

Headers get the attention and cookies get missed. Every cookie your application issues should carry:

  • `Secure` — never sent over plaintext. There is no cookie that benefits from omitting this, including the ones holding nothing sensitive.
  • `HttpOnly` — unreadable from JavaScript. Mandatory for session cookies. The exception is a cookie the front end genuinely has to read, such as a locale preference, and that exception should be written down as deliberate.
  • `SameSite=Lax` — not sent on cross-site requests, which removes most CSRF exposure. Use Strict where the user experience allows it.
Set-Cookie: session=...; Path=/; Secure; HttpOnly; SameSite=Lax

Beyond headers

The same configuration pass should cover four more things, because they fail in the same way and get found by the same people.

  1. 1HTTP methods. Unsupported methods should be refused with 405 at the edge rather than handed to application code. A TRACE request answered with a 500 means it reached your application, which is not where it should have got to.
  2. 2Exposed files. Check /.env, /.git/config, /.git/HEAD, package.json, docker-compose.yml, and .bak .sql .zip variants of your config files. Check whether JavaScript source maps are served. All should return 404.
  3. 3Error behaviour. Send malformed JSON and a deliberately wrong content type. You want a typed error and nothing else. No stack trace, no framework internals, no version string.
  4. 4A `security.txt`. Publish one at /.well-known/security.txt to RFC 9116, with a monitored address and an Expires date kept in the future. Without it, a researcher who finds a problem has to guess, and "guess" often means a public tweet.

Where to apply the fix

One thing worth getting right on the first pass. If you run more than one site or environment, these belong in shared edge configuration rather than in each site's own block.

Set them per site and they drift. One host gets the new policy, another does not, and six months later you have two different security postures and no way to tell which is which without checking each one by hand.

Related service

Security & Configuration Audit

Security audit services covering TLS, HTTP security headers, cookies, Content-Security-Policy, HTTP methods, exposed files and error handling, measured against current practice. Free consultation.

See how we run it →
FAQ

Questions,Answered

None are required by the protocol; all are opt-in hardening. In practice Content-Security-Policy, Strict-Transport-Security, X-Content-Type-Options, Referrer-Policy and Permissions-Policy are the set expected on any production site, and their absence is flagged in essentially every security review.

Keep reading

Application Security7 min read

What Is Penetration Testing?

A penetration test is not a scan with better branding. Here is what actually happens during one, phase by phase, and what the report should contain when it lands.

Read the article
Let's Talk

Ready to GetStarted?

We are happy to offer a free initial consultation to explore your goals and how NEXHUB can help you reach them.

Get a Free Consultation