· 2 min read
CSP and security headers: how this site is locked down
A portfolio is a small site, which makes it a good place to practise big-site security: a Content Security Policy with hashes, no third-party scripts, and headers that cost nothing.
اقرأ بالعربية
A personal site seems like the last place that needs serious security. But it is a site with my name on it, a contact form and a dashboard, and it is the easiest possible place to practise the habits I expect from a commerce platform. Here is what protects it.
A Content Security Policy that names every script
Cross-site scripting works by getting a browser to run code you didn’t intend. A Content Security Policy (CSP) tells the browser exactly which scripts are allowed. This site allows scripts from its own origin, plus Cloudflare’s challenge script for an optional captcha. Every inline script, starting with the tiny one that runs before the first paint, is allowed only by a SHA-256 hash of its exact contents:
default-src 'self';
script-src 'self' https://challenges.cloudflare.com 'sha256-…';
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
form-action 'self';
upgrade-insecure-requestsIf an attacker managed to inject a <script> into a page, the browser would refuse to run it, because its hash isn’t on the list. The framework computes hashes for its own scripts at build time, so the policy stays strict without manual work.
No third-party scripts by default
The simplest way to avoid supply-chain surprises is not to load other people’s code in the browser. No analytics snippets, no chat widgets, no font CDNs. The one exception the policy allows, Cloudflare’s challenge script, only loads if the optional captcha is switched on. Fonts are self-hosted and trimmed to the Latin and Arabic subsets the site uses. That is also a privacy promise: visiting the site doesn’t tell another company you were here.
Headers that cost nothing
- Strict-Transport-Security keeps browsers on HTTPS.
- X-Content-Type-Options: nosniff stops the browser guessing file types.
- Referrer-Policy: strict-origin-when-cross-origin avoids leaking full URLs to other sites.
- Permissions-Policy switches off the camera, microphone, geolocation and payment APIs, while allowing passkeys on this origin.
- frame-ancestors 'none' and X-Frame-Options: DENY prevent the site from being framed for clickjacking.
The dashboard is stricter still
The admin area adds noindex, no-store caching, sessions in HTTP-only secure cookies, mandatory two-factor authentication, passkeys, rate limits on sign-in and an audit log of every change. Uploads are re-encoded on the server, which strips metadata and anything that isn’t an image; SVGs are cleaned and served with a sandboxing policy.
Forms without trackers
The contact form uses a honeypot field, a signed timestamp that catches instant bot submissions, and per-IP rate limits, with IP addresses stored only as salted hashes. A captcha is optional, not the first line of defence.
How to check your own site
Open your site, then the browser’s developer tools, and look at the response headers of the HTML. Ask: is there a CSP, and does it allow 'unsafe-inline' scripts? Which third-party domains load code? Is HSTS set? Can the site be framed? Scanners like Mozilla Observatory and securityheaders.com will grade you in seconds.
Security on a small site is mostly a matter of deciding, once, not to load things you don’t need, and writing the rules down in headers so the browser enforces them for you.



