Skip to main content

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​

HeaderValue
Content-Security-PolicyStart strict: default-src 'self', list the exact hosts for scripts, images and connect-src
Strict-Transport-Securitymax-age=31536000; includeSubDomains
X-Content-Type-Optionsnosniff
Referrer-Policystrict-origin-when-cross-origin
Permissions-PolicyDeny 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=Lax or Strict.
  • State-changing requests from forms need CSRF protection, or use SameSite=Strict and check the Origin header.

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-For leftmost, 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.