`. Output: cabana route, handler, swag annotation and Go tests; regenerated admin.json and schema.d.ts; SPA component, styles and vitest tests; cabana README and docs/backend/forms.md; rebuilt modules/boardwalk/dist; raw-HTML hygiene gates taught the one sanctioned binding. Three code commits, one per task. Source coverage (from the bug report; there is no CONTEXT/RESEARCH in quick mode): | Item | Task | |------|------| | Preview renders markdown instead of raw source | 2 | | HTML comes only from cabana.RenderMarkdown on the server; no unsanitized HTML in the SPA | 1, 2 | | Authenticated admin endpoint; reuse one if it exists (none exists: RenderMarkdown has no production caller) | 1 | | Fetch on Preview toggle, debounced while Preview is open; raw-HTML binding only on the server's answer | 2 | | Admin auth and CSRF conventions for POST (backend guard group + requireAjax) | 1 | | mlmarkdown preview follows the active locale | 2 | | Go tests: auth required, unsafe HTML stripped | 1 | | SPA vitest tests | 2 | | Rebuild and commit modules/boardwalk/dist with the SPA change | 2 | | README + docs/ for the new route; TestDocsTree green | 1, 2 |
` with a `v-if="preview"` wrapper holding a sibling `` (text-[13px], danger colour, shown when previewError is set, text interpolation only) and the pane `
`. - The pane keeps `:class="controlClass(invalid)"` plus `summer-markdown min-h-input overflow-auto px-3.5 py-2.5` and `:aria-busy="loading ? 'true' : 'false'"`. It binds the raw-HTML directive to `sanitizedHtml`. Put that attribute on a line of its own containing exactly the attribute `v-html="sanitizedHtml"` and nothing else, because Task 3's gate exemption matches that exact line. - The textarea branch stays as is (v-else). 3. Rewrite the file's head comment. It must say the preview renders only the HTML that POST /markdown/preview answers (cabana.RenderMarkdown: goldmark without unsafe HTML, refused output becomes a 422 notice), and that the markdown source is never bound as HTML. Describe the binding as "the raw-HTML binding". The comment must NOT contain the directive's name or the DOM property names the hygiene gates search for. The current line-5 comment is exactly what makes check-phase12.1/12.2 hygiene fail today. 4. main.css, inside `@layer components` after the `.summer-partial` block: add a `.summer-markdown` kit, using `:where()` selectors so specificity stays low and reading only `--c-*` variables and `--font-mono`, in the same style as `.summer-partial`. It covers: - h1 to h4 sizes and weights; - p/ul/ol/blockquote/pre/table margins, with the last child at 0; - ul disc and ol decimal with left padding (preflight removes them); - inline code and pre in `--font-mono` on `--c-subtle` with a radius; - blockquote with a left border in `--c-border-strong` and `--c-muted` text; - links underlined in `--c-text` with a focus-visible ring; - hr and table cell borders in `--c-border`; - images capped at max-width 100%. 5. Tests. Rewrite admin/tests/form/MarkdownField.test.ts to cover every behavior bullet with `mockApi` routes keyed `POST ${API}/markdown/preview` (route functions may read `await request.json()` to answer per locale). Use `vi.useFakeTimers()` plus `vi.advanceTimersByTimeAsync` and `flushPromises` for the debounce and race cases, and restore real timers afterwards. In admin/tests/form/MLFields.test.ts, rewrite "does not execute raw HTML in the markdown preview" to mock the route answering `` and assert that no script or img element exists and the window flag is undefined. Drop the assertion that the pane text contains the source. Neutral fixture text only. 6. docs/backend/forms.md, "Markdown and multilingual fields": replace "The admin SPA shows a source editor and may preview HTML from `cabana.RenderMarkdown`" with the actual behavior. Preview posts the field's source (the active locale's text for `mlmarkdown`) to the preview route when it opens and again shortly after the source changes. It renders only the server's answer and shows the server's message as text when the output is refused. Keep the existing safety sentence. 7. Run `npm --prefix admin run build` and include modules/boardwalk/dist. Commit: `fix(admin): render markdown preview from server-sanitized HTML` (SPA, styles, tests, forms.md, dist). No .planning files, no trailers.cd /media/nvme/dev/golem15/summercms.io/summercms/summercms.go && npm --prefix admin test -- tests/form/MarkdownField.test.ts tests/form/MLFields.test.ts && npm --prefix admin test && npm --prefix admin run typecheck && scripts/check-admin-dist.sh && go test ./cmd/summer -run TestDocsTree Task 3: Teach the raw-HTML hygiene gates the single sanctioned preview binding; full suite green scripts/check-phase10.sh, scripts/check-phase12.1.sh, scripts/check-phase12.2.sh, scripts/check-phase14.2.1.sh scripts/check-phase10.sh (hygiene_checks raw-HTML rule near the "raw-HTML directive" refusal, and the vhtml plant in run_self_test), scripts/check-phase12.1.sh (run_hygiene raw-HTML rule), scripts/check-phase12.2.sh (run_hygiene raw-HTML rule), scripts/check-phase14.2.1.sh (run_forbidden raw-HTML rule) Keeps the raw-HTML gates honest now that one server-sanitized binding exists (bug report: "only then uses v-html on the server-sanitized output"). 1. In each of the four scripts, keep the existing grep. Pipe its output through one `grep -vE` exclusion that drops ONLY the MarkdownField.vue line consisting of optional whitespace, then exactly `v-html="sanitizedHtml"`, then optional whitespace. Insert the exclusion before the `|| true`, matching how check-phase10 already excludes client.ts from the fetch rule. - check-phase10.sh, check-phase12.1.sh and check-phase12.2.sh: anchor at `^admin/src/components/form/fields/MarkdownField\.vue:[0-9]+:`. - check-phase14.2.1.sh: it greps from admin/src with `.`, so anchor at `^\./components/form/fields/MarkdownField\.vue:[0-9]+:`. Use `[[:space:]]*` for the whitespace and escape the dots. 2. Add a one-line comment above each exclusion: the markdown preview binds only the HTML that POST /markdown/preview answers (cabana.RenderMarkdown), and every other raw-HTML sink is still refused. 3. Leave the patterns themselves, every other rule and check-phase10's self-test plant (admin/src/__plant/Plant.vue) unchanged. The plant must still be refused because the exclusion is file- and line-exact. 4. Do not try to fix check-phase10's unrelated pre-existing hygiene refusals (admin/src/api/files.ts, untested modules). Report them in the SUMMARY as pre-existing. 5. Commit: `chore(scripts): allow the sanitized markdown preview binding in raw-HTML gates`. No .planning files, no trailers. cd /media/nvme/dev/golem15/summercms.io/summercms/summercms.go && bash -n scripts/check-phase10.sh scripts/check-phase12.1.sh scripts/check-phase12.2.sh scripts/check-phase14.2.1.sh && scripts/check-phase12.1.sh --hygiene && scripts/check-phase12.2.sh --hygiene && scripts/check-phase14.2.1.sh --forbidden && bash -c "! scripts/check-phase10.sh --hygiene 2>&1 | grep -q 'raw-HTML directive'" && go vet ./... && go test ./... ## Trust Boundaries | Boundary | Description | |----------|-------------| | browser → admin API | Untrusted markdown source (an administrator's input, often translated or pasted) crosses into POST /markdown/preview | | admin API → SPA DOM | Server-rendered HTML is bound as HTML in the admin origin, the first raw-HTML sink in admin/src | | cross-site page → admin API | A foreign page may try to drive the cookie session (CSRF) | ## STRIDE Threat Register | Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | |-----------|----------|-----------|----------|-------------|-----------------| | T-261006sne-01 | Tampering (XSS) | MarkdownField.vue preview pane | high | mitigate | Only `data.data.html` of a 2xx answer from POST /markdown/preview is bound. That HTML comes from cabana.RenderMarkdown (goldmark without unsafe HTML plus the reject gate). The source, error messages and concatenations are never bound. Vitest proves that script/img-onerror sources create no elements and do not set the window flag. | | T-261006sne-02 | Tampering (XSS) | service.markdownPreview | high | mitigate | The route always goes through RenderMarkdown. A refused output is a 422 with a fixed message, never the HTML. Go tests cover script, iframe, onerror, javascript:, vbscript: and data: inputs, each asserted either absent from a 200 answer or refused with 422. | | T-261006sne-03 | Spoofing / Elevation | POST /markdown/preview | medium | mitigate | Mounted in the backend-guarded group, and the handler re-checks principal.Backend. TestPhase09PermissionMatrix requires the guard, and the handler and router tests assert 401 without a session. | | T-261006sne-04 | Tampering (CSRF) | POST /markdown/preview | medium | mitigate | Wrapped in requireAjax. TestPhase10CSRF walks it (count 26) and asserts 403 for cookie-only requests with the body unread. | | T-261006sne-05 | Information disclosure | POST /markdown/preview | low | accept | No permission beyond a backend session. The route is stateless: it reads no records and writes nothing, and answers only a rendering of the caller's own input. | | T-261006sne-06 | Denial of service | POST /markdown/preview | low | mitigate | Body capped at http.body_limits.default_bytes (1 MiB default) by decodeStrictBody, giving 413. The SPA fetches on toggle and with a 300 ms debounce only while Preview is open. | | T-261006sne-07 | Tampering (gate erosion) | scripts/check-phase10/12.1/12.2/14.2.1 | medium | mitigate | The exemption matches one file and one exact attribute line. Every other raw-HTML sink, including check-phase10's planted self-test binding, is still refused. | | T-261006sne-SC | Tampering | npm/go installs | low | accept | No new npm or Go dependency. The work uses the existing goldmark pin, openapi-fetch, the pinned swag v1.16.6 tool run and the committed lockfile. | - go vet ./... and go test ./... (Task 3 runs the full suite; testcontainers needs Docker) - go test ./modules/cabana -run 'MarkdownPreview|TestPhase09|TestPhase10CSRF|TestPhase10OpenAPIConformance' - scripts/check-admin-openapi.sh --check - npm --prefix admin test and npm --prefix admin run typecheck - scripts/check-admin-dist.sh - go test ./cmd/summer -run TestDocsTree - scripts/check-phase12.1.sh --hygiene, scripts/check-phase12.2.sh --hygiene, scripts/check-phase14.2.1.sh --forbidden - Manual UAT (not gating): rebuild and restart the proof host, open a markdown field at /backend and click Preview. `# Title` should render as a heading. On an mlmarkdown field, switch the locale with Preview open. - Preview on markdown and mlmarkdown fields shows rendered markdown produced by the server's sanitizing renderer. - No unsanitized HTML reaches the SPA DOM, and the source is never bound as HTML. - The new route follows admin auth (backend guard, 401) and CSRF (requireAjax, 403) conventions. It is documented in OpenAPI, README and docs/, and covered by Go and vitest tests. - modules/boardwalk/dist is rebuilt and committed with the SPA change. All drift gates and go vet / go test ./... are green.