Skip to content

Web Development

Security Headers — CSP, HSTS, X-Frame-Options You Should Ship on Day 1

All articles
🔒 ⚔️

Most websites ship with zero security headers. That's a vulnerability. Your site is open to clickjacking, MIME-sniffing, XSS attacks, and man-in-the-middle downgrades. This isn't optional — it's table stakes. Five headers lock it down: Content-Security-Policy (CSP) blocks inline scripts and limits resource origins, HTTP Strict-Transport-Security (HSTS) forces HTTPS, X-Frame-Options DENY prevents framing, Referrer-Policy strict-origin hides sensitive URLs, and Permissions-Policy locks down camera/microphone access. Drop them into Netlify's `_headers` file and you get an A+ on SecurityHeaders.com instantly. This post walks through what each header does, real gotchas (CSP with third-party scripts, HSTS preload), a production-ready template, and the six questions every security audit asks.

Security headers are cheap insurance. They take 20 minutes to set up, zero runtime cost, and they close attack vectors that 95% of the web leaves wide open. A site without security headers is like leaving the front door unlocked. An attacker can inject scripts, frame your page inside a malicious site, downgrade your HTTPS connection, or leak sensitive URLs in referrer headers. These aren't theoretical risks — they're actively exploited. A single misconfigured header is often the first foothold a vulnerability scanner finds. Velocity X sites ship with five locked-down security headers on day one. Not as an afterthought. As a requirement.

Why Headers Matter (The Threat Model)

Security headers are instructions your server sends to the browser: "Don't load scripts from untrusted origins", "Don't allow framing", "Force HTTPS forever". The browser enforces them. If an attacker tries to inject a script from `evil.com`, the browser sees your CSP policy says "scripts only from my domain" and blocks it. The attacker doesn't get a second chance.

The real threat: your site gets injected with malicious JavaScript. Maybe through a compromised dependency, maybe through XSS in user input, maybe through a supply chain attack (your ad provider gets hacked and starts serving malicious code). Without CSP, that script runs with full access to your site. It can steal cookies, exfiltrate data, redirect users, or capture keystrokes. With CSP locked down, the browser stops it at the network boundary before it even loads.

That's the trade-off: security headers are restrictions. They break inline scripts, block third-party resources, and force HTTPS. But those "breaks" are actually you choosing to be secure. Every restriction prevents a real attack. The cost is zero. The benefit is enormous.

The Five Headers You Need

1. Content-Security-Policy (CSP) — The Gatekeeper

CSP is the heavyweight. It defines where scripts, stylesheets, fonts, images, and other resources can load from. A strict CSP might look like: `script-src 'self'` (scripts only from your domain) or `script-src 'self' https://cdn.example.com` (scripts from your domain or a specific CDN). The browser blocks anything else.

The gotcha: inline scripts. If your code has ``, a strict CSP blocks it because inline scripts aren't from a trusted origin. You have to move scripts into external files or use nonces. That's a feature, not a bug — most inline scripts are a security smell anyway. Astro ships with zero inline scripts, so this isn't painful.

Third-party scripts (GA, Meta Pixel, Intercom) need explicit whitelisting. You'll add `https://www.googletagmanager.com` for GTM, `https://connect.facebook.net` for Meta Pixel, and so on. The upside: you can see at a glance which external origins your site trusts. It forces you to be intentional about dependencies.

A production CSP for Velocity X looks like: `default-src 'self'; script-src 'self' https://www.googletagmanager.com https://connect.facebook.net; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://supabase.com`. That's legible, auditable, and locked down.

2. HTTP Strict-Transport-Security (HSTS) — Force HTTPS Forever

HSTS tells the browser: "Only ever talk to this site over HTTPS. Never fall back to HTTP." Without it, an attacker on your WiFi can intercept HTTP traffic, downgrade your HTTPS connection, or perform a man-in-the-middle attack. With HSTS, the browser refuses the HTTP request entirely.

Simple header: `Strict-Transport-Security: max-age=31536000; includeSubDomains; preload`. That's one year, applies to all subdomains, and opts you into the HSTS preload list (a list shipped in browsers of domains that force HTTPS by default). The preload is optional but recommended for production sites.

Real impact: once a user visits your site, their browser remembers for one year that you only speak HTTPS. Even if they click a malicious HTTP link, the browser upgrades it to HTTPS automatically. Downgrade attacks become impossible.

3. X-Frame-Options — Prevent Clickjacking

If your page can be embedded in an iframe, an attacker can frame it invisibly and trick users into clicking buttons they don't see. That's clickjacking. `X-Frame-Options: DENY` tells the browser: "Do not allow this page to be framed, even from the same domain." Your page will never load inside an iframe. Clickjacking attacks become impossible.

Alternative: `X-Frame-Options: SAMEORIGIN` allows framing from your own domain only. Use this if you have internal dashboards that need to embed your page. For marketing sites, DENY is simpler and safer.

4. Referrer-Policy — Hide Sensitive URLs

When a user clicks a link from your page to an external site, the browser sends a `Referer` header (note: IETF misspelled it decades ago) telling the destination where the user came from. If your site is `example.com/user/john/invoice/12345` and the user clicks a link to Google, Google learns about your URL structure. That's information leakage.

`Referrer-Policy: strict-origin-when-cross-origin` sends the origin only (`example.com`) when clicking to external sites, but the full URL when navigating within your domain. Even better for privacy: `Referrer-Policy: strict-origin` sends only the origin, always. The tradeoff: external analytics might lose referral attribution. For most sites, the privacy win outweighs it.

5. Permissions-Policy — Lock Down Powerful APIs

Modern browsers expose powerful APIs: camera, microphone, geolocation, USB, magnetometer. If your marketing site has no reason to access the camera, why allow it? `Permissions-Policy` (formerly Feature-Policy) restricts which APIs scripts can use. `Permissions-Policy: camera=(), microphone=(), geolocation=()` blocks all three, everywhere on your site.

Impact: if a third-party script tries to access the camera, the browser blocks it silently. The script can't ask the user for permission because the permission is already denied at the browser level. Supply chain attacks that try to steal camera access fail immediately.

Real Netlify _headers Template

Netlify's `_headers` file sits in your build output root (typically `dist/` or `build/`). Astro's `public/` directory copies directly into the build, so create `public/_headers` and Netlify will deploy it.

Here's a production-ready template you can ship today:

/*
  Content-Security-Policy: default-src 'self'; script-src 'self' https://www.googletagmanager.com https://connect.facebook.net; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://supabase.com https://www.google-analytics.com; frame-src 'none'; base-uri 'self'; form-action 'self'
  X-Frame-Options: DENY
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin
  Permissions-Policy: camera=(), microphone=(), geolocation=()
  Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

/images/*
  Cache-Control: public, max-age=31536000, immutable

/*.js
  Cache-Control: public, max-age=31536000, immutable

/*.css
  Cache-Control: public, max-age=31536000, immutable

Copy this, paste it into `public/_headers`, and deploy. Netlify reads it and applies the headers to every response. No code changes needed. SecurityHeaders.com will instantly rate you A+.

CSP Gotchas (The Implementation Minefield)

CSP is powerful but it breaks things if you're not careful. Here are the three gotchas Velocity X learned the hard way.

Inline Scripts and Nonces

If you have `` in your HTML, a strict CSP blocks it. Solution: move the script to an external file or use a nonce. A nonce is a random string you generate once per request, include in the `

Let us make some quick suggestions?
Please provide your full name.
Please provide your phone number.
Please provide a valid phone number.
Please provide your email address.
Please provide a valid email address.
Please provide your brand name or website.
Please provide your brand name or website.