docs(phase-11.2): add security threat verification

This commit is contained in:
Jakub Zych
2026-11-05 10:09:29 +01:00
parent 71dc0485ee
commit 95cced94fc

View File

@@ -0,0 +1,100 @@
---
phase: "11.2"
slug: "ready-to-share-summercms-io-website-and-newsletter-plugin"
status: verified
# threats_open = count of OPEN threats at or above workflow.security_block_on severity (the blocking gate)
threats_open: 0
asvs_level: 1
created: "2026-11-05"
---
# Phase 11.2 — Security
> Per-phase security contract: threat register, accepted risks, and audit trail. The newsletter plugin was not built in this phase (moved to backlog 999.1); no threat in the register concerns it. Evidence roots: FW = `summercms.go`, APP = `../sm-summercmsio-app`, PLUG = `APP/plugins/golem15/summercms`, SITE = `APP/vue-summercmsio-app`. Verified against the current tree; later phases (changelog, marketplace, VPN admin) changed the app, and replaced prongs are noted. T-11.2-SC is declared in all three plans (19 rows, 17 IDs).
---
## Trust Boundaries
| Boundary | Description | Data Crossing |
|----------|-------------|---------------|
| npm registry → build machine | third-party packages run during install and generate | tarballs, git deps, install scripts (high) |
| en.json / terminal.json → rendered HTML | repository copy becomes public HTML | i18n strings, terminal commands (medium) |
| page → visitor clipboard | the Copy button writes text visitors paste into a shell | six shell commands (low) |
| page → third-party origins | any runtime request elsewhere leaks visitor data | IP, Referer (medium) |
| internet → nginx (rome) | untrusted HTTP; TLS terminates here | all public requests (high) |
| nginx → summercms-io on 127.0.0.1:8095 | proxied requests filtered by path and method | path, method, X-Forwarded-For (medium) |
| request path → embedded file map | an untrusted path selects a file and may redirect | URL path, Location (high) |
| site.yaml / CLI flags → docs header HTML | configured URL written into every docs page | site_url, site_label (medium) |
| operator machine → rome | binary and config upload; secrets stay on the server | binary, config (medium) |
| summercms.go → module proxy | a pushed tag becomes immutable public state | release tag (medium) |
| test results → phase sign-off / env gates → integration tests | a gate misreading skips passes untested behaviour | test events, gate variables (medium) |
---
## Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation | Status |
|-----------|----------|-----------|----------|-------------|------------|--------|
| T-11.2-01 | Tampering (XSS) | components rendering en.json | medium | mitigate | no v-html/innerHTML under SITE/app/; inline code via `<i18n-t>` | closed |
| T-11.2-02 | Information disclosure (privacy) | font loading | medium | mitigate | `@nuxt/fonts` build-time download (SITE/nuxt.config.ts:40,64-71); SITE/tests/output.test.ts:218-231 asserts no Google font hosts; built output clean | closed |
| T-11.2-03 | Tampering | Copy button payload | low | accept | AR-11.2-01 | closed |
| T-11.2-04 | Information disclosure | PLUG static.go path resolution | high | mitigate | `path.Clean` + dot-segment 404 + preloaded map only, dotfiles never loaded (static.go:131-183); smoke.sh:230 and static_test.go:155-156 assert `/docs/.summer-docs` 404 | closed |
| T-11.2-05 | Spoofing (open redirect) | /docs and extension-less redirects | medium | mitigate | constant `/docs/` redirect (plugin.go:86); redirect only to an existing mapped name (static.go:178-182); TestStaticRedirectLocations covers `//evil`, `../`, backslash | closed |
| T-11.2-06 | Tampering (MIME sniffing) | static.go headers | low | mitigate | own content-type table first (static.go:30-60); `nosniff` on every file incl. 404s and scripts | closed |
| T-11.2-07 | Elevation of privilege | admin exposure | high | mitigate | public nginx `/backend` → 404 (summercms.io.conf:74-76), binary on loopback, check-deploy.sh:129-130 guards the block; admin only on the VPN vhost (10.8.3.0/24). Replaced since: plugin now has admin controllers (Phase 34) and smoke.sh:225 expects 200 on the binary; the internet-facing control holds | closed |
| T-11.2-08 | Information disclosure | DB password and app key | high | mitigate | empty secrets in committed config; `.env` gitignored, only `*.example` tracked; `.env` root:summercms 0640, never rsynced (DEPLOY.md:195,289) | closed |
| T-11.2-09 | Tampering (XSS) | docs header site_url | medium | mitigate | `checkSiteURL` on yaml key and flag (internal/docsite/load.go:125,145-161,289); `html/template` with plain string; load_test.go rejects `javascript:` and `//` | closed |
| T-11.2-10 | Tampering (clickjacking) | public pages | low | accept | AR-11.2-02 | closed |
| T-11.2-11 | Information disclosure (transport) | nginx | high | mitigate | 80→443 301 except ACME, www→apex, certbot certs, HSTS (summercms.io.conf:20-63); binary binds 127.0.0.1:8095 | closed |
| T-11.2-12 | Denial of service | nginx and binary | medium | mitigate | `limit_except GET HEAD` on `/` (:176-179); site routes GET-only (routes_test.go, smoke POST → 405); preloaded static files; supervisor restart | closed |
| T-11.2-13 | Tampering / Repudiation | v0.1.0 tag | medium | mitigate | blocking-human checkpoint (11.2-02-PLAN.md:318) resolved as defer-tag; no v0.1.0 exists; all tags annotated; CLAUDE.md requires go-ahead | closed |
| T-11.2-14 | Elevation of privilege | runtime account | medium | mitigate | nologin system user, dedicated Postgres role and DB, root-owned install root with only `storage/` writable (DEPLOY.md:173-182); supervisor `user=summercms` | closed |
| T-11.2-15 | Repudiation (false green) | scripts/check-phase11.2.sh | medium | mitigate | detector refuses build failure, no-tests, skip, fail, zero tests, missing named tests (:58-120); unknown flag exits 2 | closed |
| T-11.2-16 | Tampering (silent skip) | build-gated tests and terminal check | medium | mitigate | gate sets `SUMMERCMS_REQUIRE_BUILD=1` and `SUMMERCMS_TERMINAL_CHECK=1`; links_test.go `requireBuild` fails instead of skipping | closed |
| T-11.2-SC (01) | Tampering | pnpm installs | high | mitigate | exact pins, committed lockfile (git deps locked to commits), build allow-list of three packages, `--frozen-lockfile` (APP/scripts/build.sh:121) | closed |
| T-11.2-SC (02) | Tampering | dependency installs | high | mitigate | APP go.mod first-party only; PLUG direct requires (Phase 34) all already in FW go.mod at the same versions; go.sum committed; dirty-tree builds refused (build.sh:80-90) | closed |
| T-11.2-SC (03) | Tampering | dependency installs | low | accept | AR-11.2-03 | closed |
*Status: open · closed · open — below high threshold (non-blocking)*
*Severity: critical > high > medium > low — only open threats at or above workflow.security_block_on count toward threats_open*
*Disposition: mitigate (implementation required) · accept (documented risk) · transfer (third-party)*
---
## Accepted Risks Log
| Risk ID | Threat Ref | Rationale | Accepted By | Date |
|---------|------------|-----------|-------------|------|
| AR-11.2-01 | T-11.2-03 | Payload is the bundled terminal.json, identical to the visible lines; the page only writes to the clipboard and never reads it. | 11.2-01 PLAN | 2026-11-05 |
| AR-11.2-02 | T-11.2-10 | Static marketing and docs pages with no state-changing actions; the admin CSP is deliberately not reused because it blocks Nuxt's inline scripts (D-07). | 11.2-02 PLAN | 2026-11-05 |
| AR-11.2-03 | T-11.2-SC (03) | Tests use the stdlib and the existing node:test runner; no package is added; `pnpm install --frozen-lockfile` reuses 11.2-01's lockfile. | 11.2-03 PLAN | 2026-11-05 |
*Accepted risks do not resurface in future audit runs.*
---
## Notes (non-blocking)
- **Attack surface added after 11.2** (covered by later phases' registers): admin controllers on the binary behind the VPN vhost; `APP/DEPLOY.md:190` ("nginx refuses /backend, so nothing writes runtime config overrides") is stale now that the admin is reachable over the VPN — writes would still fail on the root-owned `config/`. New write routes: `/_journal/api/v1/` (token + throttle) and POST `/api/registry/v1/ingest` (T-34-15/16). Marketplace readme `v-html` is covered by T-34-13.
- **Open review info items:** IN-01..IN-08 (11.2-REVIEW-DISPOSITION.md), none security-relevant beyond IN-07 (links_test.go follows redirects).
- **UAT:** 11.2-UAT.md is still `testing` with tests 1-3 pending; the cutover test expects v0.1.0, which was never created (releases started at v0.1.1). Superseded rather than failed.
---
## Security Audit Trail
| Audit Date | Threats Total | Closed | Open | Run By |
|------------|---------------|--------|------|--------|
| 2026-11-05 | 19 rows (17 IDs) | 19 | 0 | msd-security-auditor (ASVS L1, block_on high; repo contents only, no live-host checks) |
---
## Sign-Off
- [x] All threats have a disposition (mitigate / accept / transfer)
- [x] Accepted risks documented in Accepted Risks Log
- [x] `threats_open: 0` confirmed
- [x] `status: verified` set in frontmatter
**Approval:** verified 2026-11-05