diff --git a/.planning/notes/2026-10-11-framework-html-sanitizer-bluemonday.md b/.planning/notes/2026-10-11-framework-html-sanitizer-bluemonday.md new file mode 100644 index 0000000..f56558a --- /dev/null +++ b/.planning/notes/2026-10-11-framework-html-sanitizer-bluemonday.md @@ -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: ``, ``, `javascript:`, `javascript:`, `javascript:`, MathML `xlink:href`, ``, ``, `
`. 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.