Queue-North-Website/docs/security/SECURITY_CHECKLIST.md

5.7 KiB

Security checklist — Queue North Website

Status: Current
Owner: _null
Last reviewed: 2026-08-18
Governs: the checks run before a release, and what each one proves
Review trigger: A new class of input, a new external service, or a finding that got past this list

Why this is separate from SECURITY.md

SECURITY.md is the threat model: what is being protected and from whom. It is read carefully once and revisited rarely.

This is the list somebody actually works through. Keeping them apart means the model can stay stable while the checks change, and it means a release checklist is short enough to finish rather than a document to skim.

Before a release

  • bash scripts/secrets.sh --tracked is clean — proves nothing credential-shaped is committed
  • npm run build && bash scripts/secrets.sh --built dist/ is clean — proves the bundle is clean, which the tracked scan cannot tell you
  • bash scripts/check-env.sh --file .env exits 0 — proves every variable the server reads is set and shaped right, before it reads them. Exit 2 is not a pass
  • npm audit shows no high or critical advisory in production dependencies — proves no known-exploitable code ships
  • bash scripts/preflight.sh is clean against both front doors — queuenorth.com by default and PREFLIGHT_ORIGIN=https://qn.isnull.dev for the other. Proves headers, CSP and TLS survived the deploy on two separate ingresses that can rot independently
  • The reCAPTCHA key in dist/ is the site key, not the secret key — proves the one inlined value is the one that is safe to inline

Standing checks

  • No secret in the repository, in a log line, or in an error message
  • Every externally reachable endpoint is either authenticated or deliberately public, and the deliberate ones are listed — they are, in SECURITY.md: /api/health, /api/leads, /api/support, and static files
  • Every input that reaches a query or a filesystem path is validated at the boundary — Zod, then sanitizeString(), then a bound prepared statement
  • Dependencies audited, and any accepted advisory recorded with a reason

What the browser is handed

  • No secret in the built client bundle, not merely none in the repository — proves the scan reached the artifact users actually receive
  • (only for a deployed service) Response headers carry a CSP and a frame policy, and nothing is served over plain HTTP — proves clickjacking and injection meet resistance

The authorisation group and the session-token row were deleted from this list, not left unticked. This product has no login, no session, no cookie and no role — see SECURITY.md. Three boxes that can only ever be ticked vacuously teach a reader that the boxes do not mean anything.

What a stranger can learn or exhaust

  • (precautionary) The two POST endpoints are rate-limited, and the limit has been exercised rather than assumed — proves a bot cannot run the form unattended overnight
  • The honeypot field and reCAPTCHA scoring both actually reject, tested by bypassing them — proves the anti-abuse controls are enforced server-side and not merely present
  • No endpoint returns a stored lead or support request — proves the database cannot be read back out over HTTP

The account-enumeration row from the template is gone with the accounts. preflight.sh --auth exists and is deliberately never run here for the same reason.

The compliance bar, which is not the launch bar

  • There is a record of who changed what, and when — proves the question an auditor or a customer eventually asks can be answered at all
  • (only for a deployed service) There is an environment that is not production to test against — there is not. npm run docker:test runs the image locally, which is close, and qn-website-dev on nebula is both the only deployment and the live site

The last two are a different bar from everything above them. The rest of this list gets a release out of the door; those two get it through the first compliance review.

What got past this list

Add an entry whenever a real finding was not caught here, and then add the check that would have caught it. A checklist that never grows is one nobody is honest with.

When What was missed The check now added
2026-05-11 → 2026-06-14 The Zoho WebToLead tokens xnQsjsdp and xmIwtLD were hardcoded in index.html and later src/pages/Contact.jsx, and reached four commits on what was then a public repository, before being moved to environment variables at 05b27d2. Low severity — they are public-by-design form identifiers a browser renders anyway — and deliberately not rewritten out of the history, which is now private scripts/secrets.sh in the pre-commit hook, scanning the staged diff. This is the finding that makes the hook worth having in a project with no test suite to run beside it
2026-08-18 Nobody had ever checked whether the reCAPTCHA secret key had reached git. It had not — zero commits, zero tracked files — but "we would have noticed" is not a check secrets.sh --tracked is now the first thing run in a fresh clone, per docs/TOOLS.md
2026-08-18 The live lead database had no backup and no restore had ever been attempted, and nothing in any document said so scripts/backup.sh and scripts/restore-check.sh, both run against production the same day: a verified snapshot was taken from the running container and replayed into a scratch database — 2 tables, 3 rows, under a second. docs/OPERATIONS.md now carries a real Last verified restore date. What is still missing is a schedule and an off-machine copy, tracked in Batch 15