Skip to content

Sanitizer campaign: #1227 unwound and re-filed — needs human review and a soak #1234

Description

@jeremy

Handoff for the six open PRs that replace #1227. Nothing is merged and nothing is released. Everything below is filed, verified, and waiting on people.

What happened

On 2026-08-11, #1225 and #1227 were merged to main in a single unattended session. The code was sound. The scope and the process were not:

  • Stop configuring the shared DOMPurify, and give each editor its own allowlist #1227 carried six separable concerns in one branch — a dependency floor bump with security semantics, a new consumer CSP requirement, an allowlist widening, the instance isolation itself, per-editor config, and a build-tooling one-liner. 15 files, +603/−19, about 5× the repo's median PR (3 files, ~99 lines).
  • Nobody could enumerate what was being merged. The Trusted Types policy and the dompurify bump were noticed hours later, while rewriting the PR description.
  • No human review, and no manual verification.

The mechanical cost is specific to this repo: there is no CHANGELOG. Release bodies are auto-generated lists of PR titles, and in ~60 releases none has ever flagged a breaking change or a consumer action. Shipping 0.9.30 as-is would have shown consumers only "Stop configuring the shared DOMPurify…" — the CSP requirement and the dependency floor move invisible. Over-scoping the PR silently deletes the release note. Splitting it is what puts it back.

What was done

main is at 8c64aa45, which reverts #1227 whole and is CI-green. #1225 is untouched — one concern, four files, within house norms. This was free: nothing was published (npm and RubyGems are still on 0.9.29), and every consumer pins an exact version, so no consumer saw anything.

Re-filed as five draft PRs plus the rebased #1226. Merge in order:

PR Concern Consumer action
1 #1228 prepare builds dist on install ⚠️ see below — does not work under yarn 1
2 #1229 dompurify ^3.3.0 → ^3.4.13 + url validation javascript: in an attachment url now dropped
3 #1230 alt/width/height in attachment content none
4 #1231 Own DOMPurify instance + per-editor allowlist none
5 #1232 lexxy Trusted Types policy name ⚠️ CSP entry if you enforce TT
6 #1226 SAFE_FOR_XML on re-inflation none

Only 5 genuinely depends on 4; they are stacked in merge order so each PR's diff is exactly its own concern.

The decomposition changed no production code — but the review round since then did, deliberately. As originally pushed, the stack's src/, package.json and yarn.lock were byte-identical to the reverted merge c1db29fd (the only differences being new docs and eight relocated unit tests). That is no longer true: fixing the data:-scoping security bug in #1229 changed src/config/dom_purify.js by ~64 lines, and #1232 changed a ternary to a conditional. Those are real, intended divergences from what #1227 shipped — see the review-round summary below. If you are auditing "what did the decomposition change vs the revert", diff against the first pushed SHA of each branch, not the current one.

integration/sanitizer-campaign (4c7d4126) is the whole stack as one unit; it is the same commit as #1226's head, so #1226's CI run is its CI run.

What the QA found

The two skipped steps — a real browser and a real consumer — each found something.

1. #1232's headline claim was false. Under require-trusted-types-for 'script' in Chromium, the editor fails to initialise whether or not lexxy is allowlisted, at html_helper.js:20 — Lexxy's own DOMParser.parseFromString, which no DOMPurify policy covers. There are ~10 such unwrapped sinks. This is long-standing and untouched by the series; what was wrong was the claim. Worse, #1227 had the caveat in a comment and the rewrite deleted it. Title, body, source comment and docs all corrected. Making Lexxy work under TT is separate work and wants its own issue.

2. #1228 does not work for bc3 or fizzy. yarn 1.22 does not run a git dependency's prepare; npm does. Both consumers use yarn 1.22, so github:basecamp/lexxy#<sha> installs with no dist/. My suggestion is to publish an alpha via the existing yarn release:alpha instead, which every package manager can install. Details and options on the PR.

3. #1231 fixes a live bug in bc3. app/javascript/rich_text/sanitize_attachment_content.js sanitizes with the DOMPurify singleton and a per-call config restricted to ["bc-mention","span","img"], on the chat pending-line and Trix paste paths. Under 0.9.29 that config is silently ignored once an editor connects — <a href> and <p> survive where bc3 intends them stripped. Demonstrated with bc3's real config; table on the PR.

4. #1226's system test proved nothing, and its hook may be dead code. Reverting that PR's entire src/ leaves its system test green. preserveSerializedContentHook can only fire on a nested attachment, which no production path currently produces. Two ways out — show the path is real, or drop the hook and keep the safeForXml: true opt-in — and the second also closes the open dompurify exact-pin thread. Needs a human call; detail on #1226.

5. Pre-existing, filed for information, not blocking: an attachment url with javascript: is dropped from the saved value but still reaches img[src] in the editor DOM. Not exploitable (browsers don't execute it there) and strictly better than before #1229.

What passed

What still needs a human

  1. External review of all six PRs. They are drafts with Copilot requested. This is the step whose absence caused all of the above.
  2. Decide Build dist on install so a branch or SHA can be depended on #1228 — publish an alpha instead, or keep the one-liner with an honest title.
  3. Decide Apply mXSS-safe SAFE_FOR_XML on attachment content re-inflation #1226 — prove the nested-attachment path, or drop the hook.
  4. Soak. Then merge 1 → 2 → 3 → 4 → 5 → Apply mXSS-safe SAFE_FOR_XML on attachment content re-inflation #1226.
  5. Release, and write the notes by hand this once. Titles carry the two consumer actions, but a release with a CSP requirement in it deserves more than a generated list.
  6. Then bump bc3 — including moving the dompurify pin in both dependencies and resolutions, or bc3 stays on 3.4.0 regardless of what lexxy asks for. yarn warns; it does not fail.

Known accepted cost

lexxy is public (1,208 stars, 114 forks) and #1225/#1227 already publicly describe sanitizer bypasses that are still unfixed in the released 0.9.29. The revert does not close that window — only a release does. Deliberately not releasing yet means accepting it for the length of the soak. That is a real trade and someone should own it consciously rather than inherit it.

Cleanup

integration/qa-build (4b0c9b9d) is integration/sanitizer-campaign plus a force-added dist/, built solely so yarn 1 could install it for the bc3 QA. Not for merge. Delete it once the campaign lands.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions