Secure your domain

HTTP security headers explained

HSTS, CSP, X-Frame-Options, cookie flags: what each header blocks, what a missing one means in a scan, and a safe configuration to start from.

1 Introduction

Every time your website answers a browser, it sends the page and a set of headers: short instructions the visitor never sees. A few of them are pure security switches. They tell the browser "only ever talk to me over HTTPS", "never run scripts from anywhere else", or "do not let another site frame me". The browser already knows how to enforce all of this; it just waits for you to ask.

Missing headers rarely break into a site on their own. What they do is remove a safety net, so that a single bug elsewhere (an injected script, a link to http://, a page loaded inside an attacker's frame) turns into a working attack. That is why a domain scan lists them: they are cheap to add and they make whole classes of attacks harder.

1 line

Most headers are one line in your web server, reverse proxy or CDN settings

1 year

HSTS max-age required to join the browser preload list (31,536,000 s)

0

Protection left in X-XSS-Protection: modern browsers ignore it, CSP replaced it

2 How It Works

A scanner does what a browser does: it requests your homepage and reads the response headers. Here is what a well-configured answer looks like:

HTTP/2 200
strict-transport-security: max-age=31536000; includeSubDomains
content-security-policy: default-src 'self'; frame-ancestors 'none'; object-src 'none'
x-content-type-options: nosniff
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
set-cookie: session=...; Secure; HttpOnly; SameSite=Lax

Strict-Transport-Security (HSTS)

After the first visit, the browser refuses plain HTTP for your domain for max-age seconds. It stops attackers on a café Wi-Fi from downgrading the connection and reading or changing traffic.

Content-Security-Policy (CSP)

An allow-list of where scripts, styles, images and frames may come from. If an attacker injects a <script> (XSS), the browser refuses to run it. The most powerful header, and the one that needs the most care.

X-Frame-Options / frame-ancestors

Decides who may display your pages inside a frame. Without it, a malicious page can load your site invisibly and trick users into clicking your buttons (clickjacking). frame-ancestors in CSP is the modern form; X-Frame-Options: DENY still covers old browsers.

X-Content-Type-Options: nosniff

Stops the browser from guessing a file's type. Without it, an uploaded "image" containing JavaScript may be executed as a script.

Referrer-Policy

Controls how much of the current URL is sent to other sites when a visitor clicks a link. Prevents tokens or internal paths in URLs from leaking to third parties.

Permissions-Policy

Switches off browser features your site does not use (camera, microphone, location), so an injected script or an embedded third party cannot ask for them.

Cookie flags: Secure, HttpOnly, SameSite

Secure keeps the cookie off plain HTTP, HttpOnly hides it from JavaScript (so XSS cannot steal a session), SameSite limits cross-site requests (CSRF). Every session cookie should carry all three.

3 Detailed Analysis

Missing versus weak

A scan reports two kinds of problems. A missing header is easy to read. A weak one is present but configured so that it protects little:

HSTS with a short max-age

A max-age of a few minutes expires before the next visit. Aim for six months to a year once HTTPS works everywhere.

CSP with 'unsafe-inline', 'unsafe-eval' or *

These re-allow exactly what CSP is meant to block: inline scripts, string evaluation, any origin. A policy containing them looks good on a checklist and stops very little XSS. Replace inline scripts with files, or use nonces.

CSP only in Report-Only mode

Content-Security-Policy-Report-Only reports violations without blocking anything. Fine for testing, no protection in production.

Version disclosure

Server: nginx/1.18.0 or X-Powered-By: PHP/7.4 tell an attacker which known vulnerabilities to try. Hiding versions is not a fix, but there is no reason to advertise them.

A safe starting configuration (nginx)

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

Behind a CDN (Cloudflare, Fastly, CloudFront), the same headers are set in its response-header rules, with no change to your servers. Apache, Caddy and IIS have direct equivalents.

Rolling out the two risky ones

1

CSP: start in Report-Only, open your site and watch the browser console for violations, adjust the allow-list (analytics, fonts, payment widgets), then switch the header name to Content-Security-Policy.

2

HSTS: start with a short max-age (one day), check every subdomain answers over HTTPS before adding includeSubDomains, then raise it. Add preload only when you are sure: leaving the preload list takes months.

Check your headers on mlab.sh

A domain scan reads the security headers and cookie flags your site returns, and flags those that are missing or too weak to protect.

Scan your domain on mlab.sh

4 Red Flags

No HSTS on a site with a login

Anyone on the same network can strip HTTPS on the first request and capture credentials. Login, admin and payment pages need HSTS first.

Session cookie without HttpOnly or Secure

A single XSS can read the session and hand the account to the attacker, and a single HTTP request can leak it on the network.

A CSP that allows everything

script-src * 'unsafe-inline' 'unsafe-eval' is a CSP in name only. Treat it as missing.

Pages that can be framed by anyone

Without frame-ancestors or X-Frame-Options, account settings or payment confirmation pages are exposed to clickjacking.

Exact software versions in headers

An outdated version number in Server or X-Powered-By is an invitation. Hide it, and above all update the software.

Related Modules