Access and configuration
Set security headers
Add a deliberate baseline for framing, content types, referrers, browser features, transport, and content loading.
Security headers switch on protections the browser already has, and remove guesswork about how it should treat your responses. Choose each one for your application. Don’t paste a block from a blog post you can’t explain line by line.
A baseline worth starting from
These four go on every response:
Content-Security-Policy: default-src 'self'
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
X-Content-Type-Options: nosniff stops the browser from guessing a response’s type from its bytes. Without it, a text file that happens to contain JavaScript can be run as a script. Referrer-Policy trims what your users’ browsers leak in the Referer header when they follow a link away from you. With strict-origin-when-cross-origin, another site sees https://app.flaviocopes.com/ and not the full URL with a token in the query string. Permissions-Policy turns off browser features you don’t use, so an injected script can’t ask for the camera.
The CSP line above is a starting point. The Content Security Policy lesson covers how to grow it without breaking the site.
HSTS, once HTTPS is solid
Strict-Transport-Security tells the browser to use HTTPS for your site for the next year, even if the user types http://:
Strict-Transport-Security: max-age=31536000; includeSubDomains
Add it only after HTTPS works on every subdomain. Once a browser has seen this header, it refuses plain HTTP to your site until max-age runs out. A subdomain that still needs HTTP becomes unreachable, and you can’t take the header back quickly.
Cover every route, not just the home page
Set headers in middleware, so they reach error pages and redirects too:
app.use((req, res, next) => {
res.set('X-Content-Type-Options', 'nosniff')
res.set('Referrer-Policy', 'strict-origin-when-cross-origin')
next()
})
Then check the routes people forget. This is the failure I see again and again: the home page has a perfect header set, but the framework’s built-in 404 and 500 responses skip the middleware and ship with nothing. An attacker looks for the one route where the browser’s protections are weaker.
Check with curl:
curl -sI https://app.flaviocopes.com/this-does-not-exist | grep -iE 'x-content-type|referrer-policy'
You want both lines back on the 404, exactly as on the home page. If they’re missing, your error handler is bypassing the middleware.
Don’t advertise a policy you don’t have
Copying every recommended header is its own mistake. A Content-Security-Policy that blocks your own scripts breaks the site. includeSubDomains on HSTS takes down a subdomain nobody remembered. Pick values from what your app really loads, frames, and links to.
Try this on your own project: capture the headers for a normal page, the login page, a redirect, a static file, a 404, and a forced 500. Lay them side by side against your baseline. Then load a .txt file that contains JavaScript through a <script src> tag and confirm the browser refuses to run it because of nosniff.
Lesson completed