Skip to content

Web Development

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

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 <script>alert('hello')</script>, 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 <script>window.analyticsId = 'abc123';</script> 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 <script nonce="abc123"> tag, and also in the CSP header: script-src 'nonce-abc123'. The browser allows that specific script and no others.

Astro handles this automatically with ViewTransitions. But if you're adding scripts manually, use nonces for inline code.

Third-Party Scripts and Their Dependencies

You whitelist https://www.googletagmanager.com, but GTM itself loads scripts from other origins. Those scripts need whitelisting too. You'll end up adding https://www.google-analytics.com, https://www.gstatic.com, and more. This is tedious but necessary. Test your CSP in the browser console (it logs CSP violations) and keep adding to the whitelist until violations stop.

Partytown sidesteps this by running third-party scripts in a web worker, outside the main thread. If you use Partytown, you can be stricter with your CSP because tracking scripts don't have direct network access.

External Stylesheets and unsafe-inline

style-src 'unsafe-inline' allows inline styles, which is a CSP violation. Never use it. But external stylesheets (<link rel="stylesheet">) are safe. Tailwind compiles to a single external CSS file, so you can use style-src 'self' with no 'unsafe-inline'. Astro and Tailwind play nicely here.

Testing Your Headers

Deploy your _headers file and visit SecurityHeaders.com. Paste your site URL and it'll grade your headers. Aim for A or A+. Red X's mean you're missing headers or they're misconfigured. Fix the red ones first, then tune based on your actual resource usage (some of the whitelist entries above might not apply to your site).

In the browser console, navigate to a page and search for "Refused to load the script because it violates the Content Security Policy directive." Those messages show CSP violations. Each one is a third-party resource that needs whitelisting.

Use Chrome DevTools Security tab to verify HSTS, X-Frame-Options, and other headers are present. DevTools will flag missing ones and show you the headers it received.

Six FAQs

Does CSP slow down my site?

No. CSP is processed by the browser, not your server. There's zero performance cost. The only cost is in development time when you're figuring out which resources to whitelist. After that, it's set-and-forget.

If I use Partytown, do I still need to whitelist tracking scripts in CSP?

No. Partytown runs in a web worker, which has different CSP rules. You still want a strict CSP for main-thread scripts, but third-party tracking code gets a sandbox environment. This is one reason Partytown is a best practice — it isolates untrusted code and makes your CSP simpler.

What if a legitimate third-party script stops working after I enable CSP?

It probably violates your CSP. Check the browser console for CSP violations. Add the origin to your whitelist. If the script tries to load scripts from multiple origins (common for SaaS widgets), you might need to whitelist several. This is annoying but it exposes dependencies you didn't know about.

Can I use CSP without HSTS?

Yes, they're independent. CSP prevents script injection, HSTS prevents downgrade attacks. You should use both. Together they cover most common attacks. Neither is a complete security solution (you still need secure password handling, rate limiting, CORS policies, etc.) but they're the foundation.

What's the difference between default-src and script-src?

default-src is a fallback for all resource types. script-src 'self' is specific to scripts. If you set default-src 'self' but don't specify style-src, styles inherit the default-src policy. It's cleaner to set default-src 'self' and then override specific resource types that need more open policies (like images, which might come from CDNs).

Is CSP only for marketing sites, or do SaaS dashboards need it too?

SaaS dashboards need it more than marketing sites. A dashboard handles sensitive user data — you want CSP as strict as possible to prevent data theft through XSS. Marketing sites are lower risk (no sensitive data) but still worth locking down because they're often framed by attackers in clickjacking exploits.

The Bottom Line

Security headers are the easiest security win you can ship. Five headers, one _headers file, and you're blocking most common web attacks. CSP stops script injection, HSTS stops downgrade attacks, X-Frame-Options stops clickjacking, Referrer-Policy hides sensitive URLs, and Permissions-Policy locks down APIs. Drop the template above into your Netlify build, run a SecurityHeaders.com audit, fix any red flags, and you're done. This is table stakes. Your site is open until you add them. Start today.

Built a site with this template and your CSP violations are still piling up? Check out Aidxn Design's security audit for a line-by-line CSP review and real threat modeling, or dive deeper into how to build fast and secure marketing sites simultaneously.

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.