# 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` |
