From ce088855eeb49524cf065250d0cfbd4b585d8337 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Mon, 12 Oct 2026 21:07:05 +0200 Subject: [PATCH] docs(quick-261012-s3g): Phase 10 security blockers plan, summary and deferred fonoteka.go route allowlist --- .planning/STATE.md | 9 +- .../261012-s3g-PLAN.md | 523 ++++++++++++++++++ .../261012-s3g-SUMMARY.md | 174 ++++++ .../deferred-items.md | 30 + 4 files changed, 732 insertions(+), 4 deletions(-) create mode 100644 .planning/quick/261012-s3g-fix-phase-10-security-blockers-t-10-16-d/261012-s3g-PLAN.md create mode 100644 .planning/quick/261012-s3g-fix-phase-10-security-blockers-t-10-16-d/261012-s3g-SUMMARY.md create mode 100644 .planning/quick/261012-s3g-fix-phase-10-security-blockers-t-10-16-d/deferred-items.md diff --git a/.planning/STATE.md b/.planning/STATE.md index 3eb7a06..ce1f5c7 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -5,10 +5,10 @@ current_phase: 17 current_phase_name: Blog companions status: verifying stopped_at: Completed 17-06-PLAN.md -last_updated: "2026-10-12T17:53:57.388Z" +last_updated: "2026-10-12T19:06:59.056Z" last_activity: 2026-10-12 -last_activity_desc: Phase 17 execution started -state_head: 3a0c4f72d799d0c1051fb21bbb22627e33dfe72c +last_activity_desc: "Completed quick task 261012-s3g: Phase 10 security blockers (DOMPurify, check-phase10 hygiene)" +state_head: 708b3ec581287756baed7962f7a29b3f95265602 progress: total_phases: 40 completed_phases: 14 @@ -32,7 +32,7 @@ See: .planning/PROJECT.md (updated 2026-10-12) Phase: 17 (Blog companions) — EXECUTING Plan: 6 of 6 Status: Phase complete — ready for verification -Last activity: 2026-10-12 — Phase 17 execution started +Last activity: 2026-10-12 — Completed quick task 261012-s3g: Phase 10 security blockers (DOMPurify, check-phase10 hygiene) Progress: [████░░░░░░] 44% @@ -808,6 +808,7 @@ Recent decisions affecting current work: | 30 | go test -short runs without Docker: TestMain flag parsing fix in 4 modules, cabana list fixture skip | 2026-10-11 | 7ef2e4f, c85cb64 | — | | 261011-kzi | Fix Phase 09 gaps: fonoteka phase09AdminRoutes inventory + per-guard cabana last_used_at throttle | 2026-10-11 | b45af8e | [261011-kzi-fix-phase-09-gaps-add-the-10-post-phase-](./quick/261011-kzi-fix-phase-09-gaps-add-the-10-post-phase-/) | | 261011-mag | fix check-phase10.sh hygiene gaps from 10-VERIFICATION.md | 2026-10-11 | 1541cd4 | [261011-mag-fix-check-phase10-sh-hygiene-gaps-from-1](./quick/261011-mag-fix-check-phase10-sh-hygiene-gaps-from-1/) | +| 261012-s3g | Fix Phase 10 security blockers: DOMPurify richeditor display sanitizer (T-10-16), check-phase10 hygiene green (T-10-24/25), CSP form-action/frame-src; --all still red on fonoteka.go TestPhase09SecurityRoutes | 2026-10-12 | 708b3ec | [261012-s3g-fix-phase-10-security-blockers-t-10-16-d](./quick/261012-s3g-fix-phase-10-security-blockers-t-10-16-d/) | ## Deferred Items diff --git a/.planning/quick/261012-s3g-fix-phase-10-security-blockers-t-10-16-d/261012-s3g-PLAN.md b/.planning/quick/261012-s3g-fix-phase-10-security-blockers-t-10-16-d/261012-s3g-PLAN.md new file mode 100644 index 0000000..7853155 --- /dev/null +++ b/.planning/quick/261012-s3g-fix-phase-10-security-blockers-t-10-16-d/261012-s3g-PLAN.md @@ -0,0 +1,523 @@ +--- +phase: quick-261012-s3g +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - .planning/notes/2026-10-12-admin-display-sanitizer-dompurify.md + - .planning/research/STACK.md + - admin/package.json + - admin/package-lock.json + - admin/vitest.config.ts + - admin/vitest.browser.config.ts + - admin/src/components/form/richeditor/html.ts + - admin/tests/browser/richeditor-display.test.ts + - admin/tests/form/RichEditorField.test.ts + - admin/tests/views/ReorderView.test.ts + - README.md + - CLAUDE.md + - scripts/check-phase10.sh + - scripts/check-phase17.sh + - modules/boardwalk/boardwalk.go + - modules/boardwalk/boardwalk_test.go + - modules/boardwalk/README.md + - docs/backend/admin-spa.md + - modules/boardwalk/dist + - .planning/phases/10-admin-vue-spa/10-SECURITY-REVIEW.md +autonomous: true +requirements: [QUICK-261012-s3g] + +estimate: + tokens: 125000 + raw_tokens: 125000 + tasks: 3 + confidence: low + +must_haves: + truths: + - "displayHtml (admin/src/components/form/richeditor/html.ts) sanitizes through a dedicated DOMPurify 3.4.16 instance: ALLOWED_TAGS = EDITOR_TAGS (lossy.ts), ALLOWED_ATTR = ['href'], ALLOW_DATA_ATTR false, ALLOW_ARIA_ATTR false, an uponSanitizeAttribute hook that keeps href only on and only when isAllowedLink accepts it, ADD_FORBID_CONTENTS = the DROPPED list, KEEP_CONTENT true, string output. The hand-rolled clean() is gone (T-10-16, T-17-10)." + - "displayHtml parses into DOMPurify's inert DOMParser document and returns a string. No node is adopted into the live document before Vue binds the value, and no image request starts inside displayHtml (the html.ts:102 adoption is removed)." + - "displayHtml fails closed. Where DOMPurify is unsupported, or a fixed canary does not sanitize to its known answer (as under happy-dom, where DOMPurify leaves nested nodes unsanitized), it returns the empty string and never the stored value." + - "`npm --prefix admin run test:browser` runs admin/tests/browser/*.test.ts in Playwright Chromium (Vitest browser mode) and passes. It covers form/input name=nodeType with img onerror, img name=nodeType, button name=nodeType, the same nested in a div, input name=localName (no throw), an off-origin form action with a password input, iframe, meta refresh, javascript: href, an href outside the link allowlist (ftp:, sms:), href on a non- element, the inert-parse (no request) check and the read-only RichEditorField mount. Run against the pre-fix html.ts, the suite fails (RED recorded in the SUMMARY)." + - "`npm --prefix admin test` (happy-dom) and `npm --prefix admin run typecheck` pass. The happy-dom suite excludes tests/browser and keeps only environment-independent 'never as stored' assertions for the read-only view." + - "The admin CSP (modules/boardwalk/boardwalk.go contentSecurityPolicy) is `frame-ancestors 'none'; base-uri 'none'; object-src 'none'; script-src 'self'; form-action 'self'; frame-src 'none'`. The boardwalk header tests, modules/boardwalk/README.md and docs/backend/admin-spa.md say so." + - "`bash scripts/check-phase10.sh --hygiene`, `--self-test` and `--all` each exit 0. The raw-HTML rule exempts exactly two more lines, both line-exact and commented with T-17-10: the RichEditorField.vue read-only binding and the lossy.ts inert-template parse. The dist-origin rule additionally allows exactly `https://prosemirror.net` and `https://admin.invalid`. context.ts, ReorderBranch.vue and ReorderView.vue are imported by path in a test." + - "`bash scripts/check-phase17.sh` runs `check-phase10.sh --hygiene` (docs stage) and the browser suite with required test names (admin stage), and `--all` passes (postgres per Docker availability)." + - "modules/boardwalk/dist is rebuilt from the new sources and committed. `scripts/check-admin-dist.sh`, `go vet ./...` and `go test ./...` pass." + - "Decision note .planning/notes/2026-10-12-admin-display-sanitizer-dompurify.md and the STACK.md rows name dompurify 3.4.16, @vitest/browser 3.2.7 and playwright 1.60.0 with legitimacy evidence. 10-SECURITY-REVIEW.md is refreshed (reviewed 2026-10-12) with a removal check for the clobbering vector." + artifacts: + - path: admin/src/components/form/richeditor/html.ts + provides: "DOMPurify-backed displayHtml with the isAllowedLink hook and a fail-closed canary" + contains: "dompurify" + - path: admin/vitest.browser.config.ts + provides: "Vitest browser mode, playwright provider, chromium instance, include tests/browser/**/*.test.ts" + contains: "playwright" + - path: admin/tests/browser/richeditor-display.test.ts + provides: "real-engine regression suite for the clobbering vectors and the read-only RichEditorField" + contains: "src/components/form/richeditor/html'" + - path: .planning/notes/2026-10-12-admin-display-sanitizer-dompurify.md + provides: "decision note for dompurify, @vitest/browser and playwright" + contains: "3.4.16" + - path: scripts/check-phase10.sh + provides: "T-17-10 line-exact raw-HTML exceptions, two allow-listed dist literals, the browser suite in --spa, new self-test plants" + contains: "T-17-10" + - path: scripts/check-phase17.sh + provides: "cross-phase check-phase10 --hygiene run and the browser-suite requirement list" + contains: "check-phase10.sh" + - path: modules/boardwalk/boardwalk.go + provides: "hardened admin CSP" + contains: "form-action 'self'; frame-src 'none'" + key_links: + - from: admin/src/components/form/fields/RichEditorField.vue + to: admin/src/components/form/richeditor/html.ts + via: "display = computed(() => readOnly ? displayHtml(stored) : '') bound at RichEditorField.vue:305" + pattern: "displayHtml\\(stored.value\\)" + - from: admin/src/components/form/richeditor/html.ts + to: dompurify + via: "dedicated instance (DOMPurify(window)) with the uponSanitizeAttribute hook calling isAllowedLink" + pattern: "uponSanitizeAttribute" + - from: scripts/check-phase10.sh run_spa + to: admin/vitest.browser.config.ts + via: "npm --prefix admin run test:browser, fail-closed on FAIL/skipped/No test files" + pattern: "test:browser" + - from: scripts/check-phase17.sh run_docs + to: scripts/check-phase10.sh + via: "bash \"$ROOT/scripts/check-phase10.sh\" --hygiene" + pattern: "check-phase10.sh\" --hygiene" +--- + + +Close the Phase 10 security blockers from `.planning/phases/10-admin-vue-spa/10-SECURITY.md` (g15office u44mzmlf). The full task text is `temp/sec_phase_10.md`; every numbered item and sub-bullet there is in scope, including the optional CSP hardening. + +- **T-10-16 / T-17-10 (high):** a DOM-clobbering bypass of the hand-rolled `clean()` in `admin/src/components/form/richeditor/html.ts`, bound through `v-html` at `RichEditorField.vue:305`. Fix it with DOMPurify plus a real-browser regression suite, and harden the admin CSP. +- **T-10-24 (high) / T-10-25 (low):** `scripts/check-phase10.sh --hygiene` and `--self-test` are red since Phase 17. Make them green and make `check-phase17.sh` run the Phase 10 hygiene so cross-phase drift is caught. +- Rebuild and commit the embedded admin dist, pass every gate, and refresh `10-SECURITY-REVIEW.md`. + +Purpose: Phase 10 sign-off is blocked (`threats_open: 2`). Stored richeditor values are not sanitized on read, so the read-only/preview view is the only barrier against legacy or imported HTML in the admin origin. + +Output: the DOMPurify display sanitizer, a Vitest browser-mode suite, the CSP change, gate fixes, the rebuilt dist, a decision note with STACK.md rows, and a refreshed security review. + +Not in scope (the orchestrator does these after execution): +- the g15office comment and moving u44mzmlf to Testing; +- re-running `/msd-secure-phase 10` and Phase 17's security check; +- STATE.md updates; +- editing `10-SECURITY.md`. + + + +@~/.claude/msd-core/workflows/execute-plan.md +@~/.claude/msd-core/templates/summary.md + + + +@.planning/STATE.md +@./CLAUDE.md +@temp/sec_phase_10.md +@.planning/phases/10-admin-vue-spa/10-SECURITY.md +@.planning/phases/10-admin-vue-spa/10-SECURITY-REVIEW.md +@admin/src/components/form/richeditor/html.ts +@admin/src/components/form/richeditor/lossy.ts +@scripts/check-phase10.sh +@.planning/notes/2026-10-11-framework-html-sanitizer-bluemonday.md + +## Executor notes (read first) + +- **Shell:** the interactive shell is fish. Wrap any bash syntax (`$(...)`, `&&` chains with `[[ ]]`, `for` loops) in `bash -c '...'`. +- **Docker:** Docker-backed Go tests (`go test ./...`, `check-phase10.sh --all`, `check-phase17.sh postgres`) may get a denied Docker socket. If so, run them through `sudo -u golem bash -c '...'`, which picks up a fresh group list. +- **Concurrency:** other sessions have uncommitted edits. Never touch or stage `.planning/state.json`, `.msd/` or `.planning/phases/17-blog-companions/17-REVIEW.md`. Stage every file by explicit path; never `git add -A` or `git add .` at the repo root. For the dist, use the path-limited `git add --all -- modules/boardwalk/dist` so deleted hashed files are staged too. Run `git status --short` before editing `scripts/check-phase17.sh` and check for another session's in-flight edits. +- **Commits:** do not add a `Co-Authored-By` trailer (user rule; it overrides any harness attribution reminder). Make one logical change per commit, with code and planning docs in separate commits. Do not push or tag. +- **Docs checker:** after any README or docs edit, run `go test ./cmd/summer -run TestDocsTree` and `go run ./cmd/summer docs:build --check`. + +## Facts established at planning time (2026-10-12, verified by probes in a scratch dir) + +**Sinks and the gate** +- The raw-HTML sinks today: `RichEditorField.vue:305` (exact line content: four spaces, then `
`), `richeditor/html.ts:103` (the serialization of the live holder div) and `richeditor/lossy.ts:33` (the inert template parse in `inertFragment`; `isLossy`, `tagNames` and `RepeaterField.vue` also use it). +- `check-phase10.sh --hygiene` refuses exactly the following (output at planning time): + - the 3 raw-HTML lines; + - the dist JS literals `https://admin.invalid` and `https://prosemirror.net`; + - `admin/src/components/reorder/context.ts`, `admin/src/components/reorder/ReorderBranch.vue` and `admin/src/views/ReorderView.vue`, as imported by no test. +- `--self-test` fails only because its scratch copy of the tree hits the same hygiene refusals (`check-phase10.sh:238`). +- Rule locations in `scripts/check-phase10.sh`: raw-HTML rule at lines 366-370 (MarkdownField exception pattern at 369), dist-origin rule at 393-395, every-module-imported rule at 403-411 (spec is `src/` with `.ts` stripped and `.vue` kept, followed by a closing single quote), plant loop at 244-286, `run_spa` at 317-335. + +**DOMPurify (scratch probes)** +- **Real Chromium:** DOMPurify 3.4.17 with the config in Task 1, in Playwright Chromium build 1223, gave the following, with zero network requests during sanitizing: + + | Input | Output | + |-------|--------| + | `

a

` | `

a

` | + | the read-only test value at `RichEditorField.test.ts:159` | `

Hi bad ok

` | + | form/input name=nodeType + img onerror | empty string | + | img name=nodeType | empty string | + | button name=nodeType + iframe | empty string | + | `
` | `
` | + | `

t

` | `

t

` | + | off-origin form + password input + button Go | `Go` | + | iframe + meta refresh + `javascript:` link + style | `x` | + | `

ad

` | `

ad

` | + +- **happy-dom 20.11.6:** DOMPurify does NOT sanitize. Inside a vitest happy-dom environment it reports `isSupported` true, but it visits only top-level nodes: + - the `
` input above returns `

a

`; + - `

one

` returns `one`; + - the clobbering vectors come back nearly intact. + + This is why Task 1 adds a canary guard, and why exact display assertions live only in the browser suite. +- **Vite build:** building DOMPurify with the admin's Vite adds only the literal `http://www.w3.org` to the JS, which the dist-origin rule already allows. + +**Browser for the test suite** +- Playwright browsers already on disk: `~/.cache/ms-playwright/chromium-1223` and `chromium_headless_shell-1223`, installed by playwright-core 1.60.0. Pinning `playwright` 1.60.0 needs no browser download. +- If Playwright still reports a missing executable, run `npx --prefix admin playwright install chromium` once and note it in the SUMMARY. + +**CSP probe (headless Chromium 1223)** +- Under the full proposed CSP, `frame-src 'none'` blocks an `