08 · Security
Site security
How this site is built to be safe — and a small demonstration of what I apply to client products.
This site is a small product, so it gets the same treatment as one: defaults that are safe, choices that are written down, and nothing loaded that I don’t control.
HTTP headers#
| Header | Value | Why |
|---|---|---|
| Content-Security-Policy | script-src 'self' 'nonce-…' 'strict-dynamic' |
A fresh nonce per response; no unsafe-inline, no unsafe-eval for scripts. |
frame-ancestors 'none', base-uri 'none', object-src 'none' |
No framing, no base-tag hijack, no plugins. | |
| Strict-Transport-Security | max-age=63072000; includeSubDomains; preload |
HTTPS only, for two years. |
| X-Content-Type-Options | nosniff |
Files are served as what they are. |
| Referrer-Policy | strict-origin-when-cross-origin |
Other sites see the origin, never the path. |
| Permissions-Policy | camera, microphone, geolocation, payment… disabled | The site needs none of them. |
| Cross-Origin-Opener-Policy | same-origin |
Isolates the browsing context. |
One honest caveat: style-src-attr 'unsafe-inline' is allowed, because syntax highlighting and a few components render style attributes. Style attributes cannot execute script; inline <style> and <script> elements still require the nonce.
No third parties#
- Fonts are self-hosted (downloaded at build time). No request leaves for Google Fonts.
- No analytics by default, no tracker, no embedded widget. If analytics are ever enabled, it will be a cookieless tool (Plausible) — and this page will say so.
- Visitors get no cookie. The only cookie is the back-office session, on my own account.
Forms#
- Contact and booking forms are validated server-side (schema validation), rate-limited per IP, protected by a honeypot and a time trap — no third-party CAPTCHA.
- The contact form never sends an auto-reply, so it cannot be used to send mail to arbitrary addresses.
- IP addresses are never stored in clear: only a salted hash, enough to spot abuse.
Back office#
- Passwords hashed with scrypt; constant-time comparison; same work factor whether the account exists or not.
- Session in an
HttpOnly,Secure,SameSite=Strictcookie, signed with HMAC-SHA256, 12-hour lifetime. Changing the password revokes every other session. - State-changing requests are checked against
OriginandSec-Fetch-Site. - Every sign-in, failed attempt and content change is written to an audit log.
- Uploaded images are re-encoded (metadata stripped); PDFs are accepted on their magic bytes only; SVG is refused.
Dependencies#
Minimal and audited: pnpm audit runs before every release. Last audit, 8 October 2026: two high-severity advisories (braces, node-forge), both in build and dev-server tooling — neither ships in the production server bundle — and no patched version published yet.
Responsible disclosure {#disclosure}#
Found something? Please email me (address in security.txt). I’ll acknowledge within 72 hours and credit you here if you wish. Please don’t run automated scanners against the booking or contact endpoints.