# SECURITY

Threat model + checklist for the Generic CMS Module. Operator-focused.

For full auth internals, see [`auth/SECURITY.md`](./auth/SECURITY.md). This file is the operator overview.

## Threat model

| Threat | Mitigation |
|---|---|
| **Brute-force login** | 5 attempts → 5-minute lockout (PBKDF2 cost = ~300ms per try) |
| **Stolen password hash** | PBKDF2-SHA256, 310,000 iterations, 16-byte salt. Hashes are not enough to recover passwords. |
| **Hash timing attack** | `constantTimeEqual` for ALL hash comparisons. Verified by `test-auth.js` SEC8. |
| **Session hijack** | Sessions in `sessionStorage` (not `localStorage`), 7-day expiry, raw token never persisted (only SHA-256 hash) |
| **XSS via user content** | `escapeHtml` at every DOM boundary. CSP headers in `admin.html`. Markdown field type uses allowlist sanitizer. |
| **SSRF via image URL** | URL allowlist (http/https only, no localhost, no private IPs, no `.local`). Re-validated at apply time. |
| **Credential leak via backup** | `{prefix}:admin:auth` is ALWAYS excluded from backup envelopes. Verified by `test-backups.js`. |
| **Supply chain** | No npm dependencies. Module is vendored as-is. Code review before adopting. |
| **`postMessage` injection** | (out of scope for v1.0; no cross-frame messaging) |
| **CSRF** | N/A — v1.0 is local-only. Future remote adapter will require CSRF tokens. |

## Security checklist (operator)

Before going live with this CMS, verify each item.

### Auth

- [ ] **Password is 12+ characters** (enforced by `setupAdmin`).
- [ ] **Recovery phrase saved offline** (12 words, written to paper).
- [ ] **Dev-mode is OFF in production** — set `localStorage.cms:admin:devMode` to anything other than `'true'`, or just don't set it.
- [ ] **PBKDF2 iterations = 310,000** (verified by `test-auth.js` SEC7).
- [ ] **Constant-time compare used everywhere** (verified by `test-auth.js` SEC8).
- [ ] **Lockout threshold = 5, duration = 5 min** (verified by `test-auth.js` SEC1, SEC2, SEC3).
- [ ] **Session expiry = 7 days** (verified by `test-auth.js` SEC5).

### Content safety

- [ ] **Every user-supplied string is escaped** at the DOM boundary (use `escapeHtml` from `views-helpers.js`).
- [ ] **Markdown content is sanitized** with allowlist (`sanitizeHtml`).
- [ ] **URLs validated** against allowlist (http/https only, no private IPs).
- [ ] **Image URLs validated** against the same allowlist.

### Transport

- [ ] **HTTPS only** in production.
- [ ] **CSP headers set** in `admin.html` (default-src 'self', script-src 'self' 'unsafe-inline').
- [ ] **No inline scripts from third parties.**

### Backups

- [ ] **Auth is NOT in backup** (verified by `test-backups.js`).
- [ ] **Backup envelopes stored in a safe place** (operator's responsibility).
- [ ] **Recovery phrase NOT stored in the same place as backups.**

### Data hygiene

- [ ] **Activity log capped at 5000 entries** (auto-trim oldest first).
- [ ] **No raw `localStorage.*` calls outside `storage/`** (doctrine §2).
- [ ] **All writes go through `cmsUpsert` / `cmsDelete`** (doctrine §4).

## CSP headers (set in `admin.html`)

```
Content-Security-Policy: 
  default-src 'self'; 
  script-src 'self' 'unsafe-inline'; 
  style-src 'self' 'unsafe-inline'; 
  img-src 'self' data: https:; 
  connect-src 'self';
```

If you host a remote adapter in v2.0+, add the API origin to `connect-src`.

## URL allowlist behavior

The validator rejects URLs that:

- Have a non-`http:` / non-`https:` protocol
- Point to `localhost`, `127.0.0.1`, or any `.local` host
- Point to private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16)

This is defense in depth. When the v2.0 server-side adapter lands, it must re-validate at apply time.

## Dev-mode warning

Dev-mode bypasses password check. It requires `localStorage.cms:admin:devMode === 'true'` (literal string `'true'`). Any other value, including `undefined`, disables it.

Never enable dev-mode in production. Verified by `test-auth.js` SEC6.

## Privacy posture (v1.0)

This CMS is local-only — no data leaves the device without explicit
operator action.

### Storage key namespace

All CMS data lives under the prefix `cms:admin:` in browser
`localStorage`. This includes:

- `cms:admin:auth` — admin user, recovery phrase hash, sessions, lockouts
- `cms:admin:activity` — audit log (capped at 5000 entries)
- `cms:admin:backups` — manual backup envelopes
- `cms:admin:auto-backup-{0..3}` — auto-rotated backup slots
- `cms:admin:{schemaId}` — one key per registered content type

Namespacing avoids collisions with the host site's own `localStorage`
keys. If your site already uses a `cms:` namespace, see
`docs/PRIVACY-AND-I18N.md` for collision guidance.

### What never leaves the device

- **Auth secrets** — hashed passwords, recovery hashes, session tokens
  (the hash, never the raw). The auth key is excluded from every
  backup envelope (verified by SEC14 in `tests/test-security-source.js`).
- **Activity log** — fully local. Cleared at `clearActivity()` or when
  the 5000-entry cap trims older entries.

### What the operator exports

- **Manual backups** — operator clicks Export, downloads the JSON.
  Only `cms:admin:{schemaId}` records + activity log are included.
  Auth is excluded.
- **Settings** — site settings live in `cms:admin:siteSettings`. This
  is captured in backups.

### GDPR checklist (operator)

- [ ] Content respects data minimization (record only fields needed)
- [ ] Storage namespace does not collide with site state
- [ ] Activity log retention is acceptable (5000-entry default)
- [ ] Backups are stored in a controlled location
- [ ] Recovery phrases are stored offline (paper, password manager)

For the full privacy and data-residency posture, see
`docs/PRIVACY-AND-I18N.md`.

## Reporting a vulnerability

This is an internal module. If you find a security issue, report it to the maintainer directly — do not open a public issue.

## See also

- [`auth/SECURITY.md`](./auth/SECURITY.md) — full auth internals
- `cms-doctrine.md` §6, §7, §8 — sanitization, URL allowlist, auth lifecycle
- `test-auth.js` — security tests (SEC1–SEC10)
- `test-security.js` + `test-security-source.js` — Track 5 hardening (SEC11–SEC17)
- `test-backups.js` — auth-excluded-from-backup test
- `docs/PRIVACY-AND-I18N.md` — full privacy posture and i18n limits
