⌨️ Web development

HTTP security headers every website should have

HSTS, X-Content-Type-Options, Referrer-Policy, Permissions-Policy and clickjacking protection: what each security header does and how to set it.

HTTP response headers are instructions the server sends to the browser along with the page. Some are for caching, others for the content type, and a specific group is there to limit what the browser may do with that page. Configuring them takes a few lines and closes off entire categories of problems. This applies to any site, static ones included.

Strict-Transport-Security (HSTS)

Even if your site redirects from HTTP to HTTPS, the first request from a user who types the address without a prefix may go out unencrypted. HSTS tells the browser: for this domain always use HTTPS, for a certain period, without even trying HTTP.

Strict-Transport-Security: max-age=31536000; includeSubDomains
  • max-age is the duration in seconds (one year here).
  • includeSubDomains extends the rule to all subdomains.

Before enabling includeSubDomains, check that every subdomain works over HTTPS: an old HTTP-only internal subdomain would become unreachable. A cautious strategy is to start with a short max-age, check that everything works and then increase it. There is also the preload directive, which allows the domain to be added to a list built into browsers; it is a choice that is hard to undo, so make it only when you are sure.

X-Content-Type-Options

In the past, browsers tried to "guess" the type of a file by looking at its content, ignoring the type declared by the server. This behaviour can turn a file uploaded as text into an executed script.

X-Content-Type-Options: nosniff

With nosniff the browser trusts only the declared Content-Type. The header has no downsides, provided the server sends correct types for CSS and JavaScript.

Clickjacking protection

Clickjacking means loading your site in an invisible iframe on another page and tricking the user into clicking buttons they cannot see. The modern defence is the frame-ancestors directive of the Content Security Policy:

Content-Security-Policy: frame-ancestors 'self'

With 'self' the page can be framed only by your own site; with 'none', by nobody. The older X-Frame-Options header (with the values DENY or SAMEORIGIN) does the same job and is still useful as reinforcement for older browsers.

Referrer-Policy

When a user clicks a link to another site, the browser may send the address of the originating page in the Referer header. If your URLs contain sensitive information, such as identifiers or search parameters, it ends up with third parties.

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

This value sends the full URL for internal links, only the domain to other sites, and nothing when moving from HTTPS to HTTP. It is also the default behaviour of many modern browsers, but declaring it makes the choice explicit and consistent. For particularly sensitive pages you can use no-referrer.

Permissions-Policy

This header lets you disable browser features the site does not use: camera, microphone, geolocation, payments and more. If a third-party script ever tried to use them, the browser would refuse.

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

Empty parentheses mean "no origin allowed". If you need a feature, you can restrict it to your own domain with geolocation=(self).

Content-Security-Policy

This is the most powerful and the most delicate header: it defines which origins the page may load scripts, styles, images and other resources from, and it is a key defence against cross-site scripting. It deserves a separate discussion; here it is enough to know that it should be introduced gradually, starting in report-only mode.

Headers to remove

Security is also about what you do not send. Headers such as Server with the exact version number, or X-Powered-By with the backend language and version, are of no use to users and give information to anyone looking for vulnerable software. Many servers and frameworks let you remove or trim them with a single setting.

A complete example

On many static hosts, headers are declared in a configuration file. A reasonable starting configuration:

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

With nginx the equivalent is written with the add_header directive, remembering the always parameter so it also applies to error responses:

add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

How to check

  1. Open the developer tools, Network tab, select the main document and look at the response headers.
  2. From a terminal, curl -I https://yoursite.com shows only the headers.
  3. There are online services that analyse a site's headers and flag missing ones: useful for a quick check, but interpret the results with judgement.

Check again after every change of host, CDN or configuration: it is easy to lose headers during a migration.

In short

  • HSTS enforces HTTPS; enable includeSubDomains and preload only after checking.
  • X-Content-Type-Options: nosniff stops the browser from reinterpreting file types.
  • frame-ancestors (and X-Frame-Options) protects against clickjacking.
  • Referrer-Policy and Permissions-Policy limit the data sent and the features available.
  • Remove headers that reveal software versions and check again after every migration.
MM
Marco Marzaduri

Marco Marzaduri works in cybersecurity, digital forensics and web development. A computer science graduate, he combines rigorous technical analysis with attention to the people who use technology.

Read also