docs(17-01): decision note naming bluemonday v1.0.27 for the framework sanitizer

This commit is contained in:
2026-10-12 15:09:52 +02:00
parent 0cdff4c258
commit 69bb1cc95d

View File

@@ -0,0 +1,32 @@
# Decision: bluemonday sanitizes the framework richeditor field
**Date:** 2026-10-11
**Status:** Accepted (Phase 17 plan 01, D-10)
**Relates to:** `.planning/notes/journal-html-sanitizing-bluemonday.md` (the Journal plugin's policy, which this one ports)
## Decision
The framework module `git.golem15.com/golem15/summercms` adds `github.com/microcosm-cc/bluemonday` **v1.0.27** to its `go.mod`. It is the same library and the same version `sm-journal-plugin` already uses (latest tag, 2024-07-04, confirmed with `go list -m -json github.com/microcosm-cc/bluemonday@latest`). `go.sum` is committed with it.
A new framework module, `seaglass` (glass the sea has worn smooth), owns the policy:
- `seaglass.Sanitize`, `seaglass.SanitizeReport` and `seaglass.Check` port Journal's `newHTMLPolicy` / `SanitizeHTMLReport` / `CheckHTML`: bluemonday's UGC policy with `RequireNoFollowOnLinks(false)`, the link scheme allowlist (http, https, mailto, tel and relative URLs) and Journal's extra attributes.
- The four Blog Editor rules (`figure data-embed`, `figure data-id`, free-text `img alt`, `ol start`) are **not** ported: they describe one plugin's editor output, and the framework must not import or mirror a plugin. A plugin that needs more keeps its own policy, as Journal does.
- The byte-stable comparison is kept: when the input and bluemonday's output tokenize (with `golang.org/x/net/html`, already a framework dependency) to the same tokens, the input is returned byte for byte and nothing is reported, so re-escaping alone never rewrites stored HTML. A repeated attribute always counts as a change.
- `SanitizeReport` returns what was removed (element names, attribute names, comment count, a Changed flag), which cabana turns into `meta.notices` on the field.
cabana's core `type: richeditor` field (FAQ descriptions, repeater answers) sanitizes every submitted value through `seaglass.SanitizeReport` on create and update, top level and inside repeater rows. Settings and relation forms refuse the type at boot, so no save path bypasses the gate.
## Why not cabana's markdown gate
`cabana.RenderMarkdown` checks goldmark output with three regexes (unsafe tags, `on*=` attributes, dangerous schemes). It only works there because goldmark renders without unsafe HTML, so raw HTML never reaches the check. A richeditor stores raw HTML. The Journal decision note lists real bypasses of exactly that regex style: `<img src="x"onerror=…>`, `<svg/onload=…>`, `javascript&#58;`, `javascript&colon;`, `&#106;avascript:`, MathML `xlink:href`, `<base>`, `<meta http-equiv=refresh>`, `<form>`. A tokenizing sanitizer checks attributes and URL values the way a browser parses them. The project prefers a maintained library for security code over a hand-built gate (the Journal note also rejected a hand-built allowlist on `golang.org/x/net/html`).
## Deviation from D-10: Journal category content stays blogeditor
D-10 also said "Journal's category `content` switches from `textarea` to `richeditor`". Phase 16.2 already shipped the Journal Blog Editor (`type: blogeditor`) for category content and closed the `cabana-richeditor-field-type` todo (RESEARCH A1, PATTERNS note). Switching it would regress that editor. This clause is recorded as a resolved deviation: Journal category content stays `blogeditor`; the framework richeditor serves the FAQ description and the repeater answer only.
## Consequences
- One sanitizer version across the workspace (framework and Journal both pin v1.0.27); a future bump changes both together.
- Imported PHP HTML that the policy keeps whole round-trips byte for byte on its next admin save; HTML the policy changes is stored sanitized and the editor sees what was removed.
- `modules/seaglass/README.md` names bluemonday under Dependencies; the root README lists the module.