SPB Git

Repository Public

SECURITY.md 5.0 KB

# Security and privacy

# Authentication and sessions

  • Passkeys only (WebAuthn / FIDO2, @simplewebauthn/server): discoverable credentials, user verification required, relying party = host of APP_URL. Touch ID / Face ID / security keys. Biometric data never leaves the device; the server stores a public key and a signature counter.
  • Enrolment happens only (a) with a one-time token created from the node's shell (first setup: valid 7 days, written to a 600 file; later: tsx scripts/setup-token.ts, 30 minutes) or (b) from an authenticated session. The token travels in the URL fragment, which browsers do not send to servers.
  • Recovery: eight one-use codes shown once at first enrolment (stored as SHA-256); or tsx scripts/setup-token.ts --recovery on the node, which replaces every passkey and revokes every session.
  • Sessions: 32-byte random token in an HttpOnly; Secure; SameSite=Lax cookie, stored hashed, 30 days, listed and revocable in Settings → Security.
  • CSRF: every state-changing admin call must (1) come from our origin (Origin, Sec-Fetch-Site) and (2) carry the session's CSRF token in X-CSRF-Token. Public POSTs also require same-origin.
  • Enumeration: login uses discoverable credentials (no account list in the challenge); every failure returns the same login_failed.

# Authorisation

  • One guard per API surface, executed before the handler is resolved (makeRouter). Every admin query is scoped by user_id; identifiers are UUIDs validated before use.
  • Visitors act only through their token: the API has no public endpoint that takes a request or booking id (no IDOR surface). Tokens are 256-bit, stored hashed, expire 30 days after the appointment, can be revoked.
  • The public API exposes: profile, active types (without location_detail), free slots. Never the agenda, other visitors, reasons, notes.

# Input, abuse

  • Zod validation on every body and query; parameterised SQL only.
  • Public form: honeypot field, signed form token (minimum fill time in production, 3 h maximum age), consent required, 8 submissions / hour and 30 / day per IP, at most 3 open requests per e-mail address, optional allow / block lists of e-mail domains.
  • Rate limits on slots, management links, WebAuthn and recovery codes (5 / 15 min).
  • ICS subscriptions: https only, public addresses only (SSRF guard), 8 MB cap, 25 s timeout.
  • Avatar upload: PNG / JPEG / WebP by magic bytes, 1 MB, served with nosniff.

# Headers

CSP (default-src 'self', no third-party origin, frame-ancestors 'none', form-action 'self', object-src 'none'), X-Frame-Options: DENY, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, HSTS. Residual risk: script-src allows 'unsafe-inline' (decision D-13); the application never renders user-supplied HTML (React escapes everything, e-mail templates escape explicitly).

# Secrets

Only in .env on the node (mode 600) and in the mld manifest on the gateway; never in Git (pnpm secrets:scan), never sent to the browser. APP_SECRET signs form tokens, derives visitor tokens and the AES-256-GCM key of external calendar addresses.

# Personal information (Québec, Law 25) — assessment

Topic Practice
Minimisation Name, e-mail, optional organization, reason, answers. IP kept on the request for abuse handling.
Consent Mandatory checkbox, timestamp stored (consent_at), policy at /privacy.
Confidentiality Reason and answers: admin area only. Not in URLs, logs, e-mails, ICS, audit metadata.
Retention settings.retention_days (default 730): requests, bookings, orphan contacts, audit and notification logs are deleted by the worker. Backups: 30 days.
Access / portability Admin → Contacts → export JSON.
Erasure Admin → Contacts → delete (contact, requests, answers, links, notifications, past bookings); refused while a confirmed appointment is upcoming.
Responsible person The owner; a public contact address appears on /privacy when "Reply-to" is set.
Processors The e-mail provider (address + message content). Database and backups stay on the owner's machines.
Not done No formal privacy impact assessment, no incident register: to be completed by the owner if the service is used beyond personal scheduling.

# Logging

Application logs carry error names and messages, never request bodies. The audit trail records who did what to which entity, with metadata limited to identifiers and counts.

# Backups

scripts/backup.sh daily: pg_dump → AES-256 (openssl, PBKDF2, passphrase in a 600 file outside the repo) → verified by decrypt + pg_restore --list; 30 days kept. Restore procedure and its test: OPERATIONS.md.

# Known limits

  • One owner account (the schema carries user_id everywhere; multi-user is not exposed).
  • No CAPTCHA: honeypot + rate limits + approval by default were judged proportionate.
  • Passkey flows are covered end-to-end with Chromium's virtual authenticator; Touch ID / Face ID on real devices must be checked by the owner on their own hardware.
Files main
Menu

Theme