Web security
The controls for websites, web apps and admin panels: headers, cookies, proxies, image optimisers, and keeping private sites private. Read it before you launch a site.
How it works
How to build it
Headers
| Header | Value |
|---|---|
Content-Security-Policy | Start strict: default-src 'self', list the exact hosts for scripts, images and connect-src |
Strict-Transport-Security | max-age=31536000; includeSubDomains |
X-Content-Type-Options | nosniff |
Referrer-Policy | strict-origin-when-cross-origin |
Permissions-Policy | Deny camera, microphone, location unless used |
frame-ancestors (in CSP) | 'none' unless the site is meant to be embedded |
Cookies and CSRF
- Session cookies are
HttpOnly,Secure,SameSite=LaxorStrict. - State-changing requests from forms need CSRF protection, or use
SameSite=Strictand check theOriginheader.
Trusting proxy headers
- Use client IP and country only from headers your own CDN or load balancer sets. A
header the browser can send (
X-Forwarded-Forleftmost, a country header) is never trusted. - The load balancer accepts a site's host only with a secret header the CDN adds, so the CDN cannot be bypassed.
- On dev and staging without a CDN, header trust is switched off explicitly.
Image optimisers and URL fetchers
Any feature that fetches a URL on the server (image optimisation, link previews, webhooks
we call) is a way to reach our internal network. Allow only an explicit list of hosts. For
Next.js, keep images.remotePatterns to the exact domains you use.
Private sites stay private
- Dev and staging web apps sit behind mutual TLS at the load balancer. Without a client certificate there is no login page to attack.
- Admin panels for production sit behind SSO (for example an access proxy with the company identity provider), a VPN, or mutual TLS, and require their own sign-in with TOTP.
- Database admin tools, queue dashboards and monitoring are never published to the internet. Reach them through an SSH or SSM tunnel.
Bundles
- No secrets in front-end code. Anything in the bundle is public.
- Source maps are uploaded to error tracking and not served publicly in production.
Rules
Rule
Every subdomain you create with a public certificate appears in certificate transparency logs within minutes, and scanners watch them. A "secret" subdomain is not secret. Protect it as if it were on the home page.
Rule
A server-side URL fetcher has an allowlist of hosts. Never "any https URL".
Mistakes we made
- A database admin tool was published to the internet over plain HTTP. It was found by scanners. Now bound to localhost and reached through a tunnel.
- A website trusted a browser-set country header, which let buyers pick their price. See Prices and country.
- A pre-launch site had no security headers and no CSRF protection, and its rate limit
trusted the leftmost
X-Forwarded-For. - The image optimiser accepted any host. Probes tried internal names and the cloud metadata address. They failed only because those names did not resolve.
Checklist
- CSP and security headers set and checked with a scanner.
- Cookies
HttpOnly,Secure,SameSite; CSRF covered. - Proxy headers trusted only from our own CDN; origin secret checked.
- URL fetchers have a host allowlist.
- Dev and staging behind mutual TLS; prod admin behind SSO or VPN plus TOTP.
- No database or ops tools on the internet.
- No secrets in bundles; source maps private.