76 lines
5.7 KiB
Markdown
76 lines
5.7 KiB
Markdown
# 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` |
|