HTTP Response Headers, Read Properly
A real header audit on a live site - what the response actually said, and what changed once someone read it closely.
curl -I Actually ShowedThe assumption going in was that this site's headers were already in reasonable shape - the edge-level security headers had been set up early, and nothing had thrown an obvious error since. That assumption lasted about as long as it took to run a plain response-header check against a handful of live pages and read the output line by line instead of skimming it.
Nothing was broken in the sense of a page failing to load. Everything rendered, every script fired, every form submitted. The gaps were the kind that don't show up as errors at all - a content policy that was technically present but permissive enough to not be doing much work, a referrer policy left to whatever the browser defaulted to, and a caching rule written for one kind of file being quietly applied to a completely different kind of file.
None of this shows up in a screenshot. It only shows up when someone reads the response headers as a set of enforced rules, not as background noise the browser handles for you.
| Header | Before | After |
|---|---|---|
| Content-Security-Policy | No explicit script or connect allowlist - fell back to broad defaults that didn't reflect what was actually loading | Named allowlist covering exactly the origins in active use - nothing more |
| Referrer-Policy | Not set - left to browser default |
Explicitly set to
strict-origin-when-cross-origin
|
| X-Content-Type-Options | Not set | nosniff |
| Permissions-Policy | Not set | Camera, microphone, and geolocation explicitly denied |
| Cache-Control - HTML documents | Same long-lived edge TTL as static assets | Short TTL, revalidated on each request |
| Cache-Control - static assets | Same rule as HTML documents | Long TTL, marked immutable - filenames don't change without a deploy |
style-src 'unsafe-inline' |
Present | Present - kept on purpose |
The site only talks to a small, known set of external origins - a tag manager, an analytics endpoint, a form handler. A policy that doesn't name them explicitly isn't protecting against anything specific; it's just present. Rewriting it as an exact allowlist meant the policy now reflects reality instead of a template default, and anything unexpected trying to load from somewhere else gets blocked rather than waved through.
Leaving this unset means every browser picks its own default, and those defaults haven't been consistent across versions. Setting it explicitly means the full page URL - including anything sensitive in a query string - doesn't leak to third-party origins on outbound requests, regardless of what browser someone's using.
This was the one that actually mattered for day-to-day work, not just security posture. HTML documents and static assets were sharing one caching rule, which meant a page edit could sit stale at the edge for as long as a fingerprinted CSS file was supposed to. Splitting them means content updates show up promptly, and asset caching stays aggressive where it's actually safe to be aggressive.
style-src 'unsafe-inline' alone
This is the one that stayed. Every page on this site scopes its
own layout tweaks in a page-level
<style> block rather than a shared stylesheet,
by design - no build step means no easy way to generate per-page
nonces or hashes for a static file tree. Removing this directive
would mean restructuring how every page is styled, for a category
of risk that's already low compared to what an unrestricted script
policy would have allowed. Not every gap is worth closing the same
day it's found.
A page that renders correctly and a page that's actually enforcing its own security and caching rules are two different questions. Headers are instructions the browser follows whether or not anything looks different on screen.
Only if it's written generically. A policy built from the actual list of origins in use is exact, not a guess - it allows what's really there and nothing else.
Because every removal has a cost on the other side. This one would have meant reworking the entire page-styling approach for a marginal reduction in a risk category that was already low. Judgment call, documented, not an oversight.
Any time a new third-party origin gets added - a form service, an analytics tool, an embed. That's the exact moment a hand-written allowlist quietly goes stale.
If response headers haven't been checked since launch, they're probably making decisions on your behalf that nobody actually made.