fix(sanitize-html): emit transformTags text on empty tags when textFilter is set - #5494
Merged
boutell merged 1 commit intoJun 30, 2026
Conversation
…lter is set When a transformTags handler adds text to an allowed tag that originally had no text content, the injected text was silently dropped if any textFilter was configured. The onopentag branch that emits frame.innerText was guarded by !options.textFilter, deferring emission to ontext so the filter could run there. For an empty element htmlparser2 never fires ontext, so the text was emitted by neither branch. Emit frame.innerText through options.textFilter here when present (mirroring the discard path), so the transformTags text contract holds for empty tags regardless of whether a textFilter is set.
boutell
self-requested a review
June 29, 2026 18:51
boutell
added a commit
that referenced
this pull request
Jul 8, 2026
* Fix asset URLs when a site prefix is configured (#5448) * fix: treat col as a self-closing tag (#5447) * fix: treat col as a self-closing tag * Make dateTime field responsive (css) (#5481) * Fix/from rich text adds metatype (#5488) * fromRichText adds metatype to new widget * change * nodemailer major bump (#5485) * Harden and centralize the cache invalidation (#5493) * fix(sanitize-html): emit transformTags text on empty tags when textFilter is set (#5494) When a transformTags handler adds text to an allowed tag that originally had no text content, the injected text was silently dropped if any textFilter was configured. The onopentag branch that emits frame.innerText was guarded by !options.textFilter, deferring emission to ontext so the filter could run there. For an empty element htmlparser2 never fires ontext, so the text was emitted by neither branch. Emit frame.innerText through options.textFilter here when present (mirroring the discard path), so the transformTags text contract holds for empty tags regardless of whether a textFilter is set. * changeset crediting spokodev for sanitize-html fix (#5498) * Fix shortcut conflicts (#5499) * Fix backspace after slash deleting a rich-text widget * Fix copy/paste widget/text conflicts * Fix astro redirects (#5500) * Merge commit from fork * Merge commit from fork * Merge commit from fork * Merge commit from fork * fix path traversal in import/export * correct credits * additional guards * Merge commit from fork * fix for </textarea/> vulnerability (#5501) * wip * fix for math/svg vulnerabilities --------- Co-authored-by: Jinka Manohar <145598597+Manohar2503@users.noreply.github.com> Co-authored-by: Vansh Parmar <vanshparmar8742@gmail.com> Co-authored-by: Miro Yovchev <2827783+myovchev@users.noreply.github.com> Co-authored-by: Stuart Romanek <stuart@apostrophecms.com> Co-authored-by: spokodev <spoko.dev@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A
transformTagshandler that adds text to an allowed tag is silently dropped when the tag originally had no text content and an identity (no-op)textFilteris configured.The control without a
textFilteralready yields the expected output:So the presence of an identity
textFilterchanges the result, which violates two documented contracts:transformTagstext: documented to "add or modify the text contents of a tag" (already covered by a passing test for the no-textFiltercase).textFilter: documented to "process all text content"; an identity filter is a no-op and must not change output.This is a correctness bug, not a security bypass.
Root cause
In
onopentag(packages/sanitize-html/index.js), the branch that emitsframe.innerTextfor an allowed tag was guarded by!options.textFilter:The
!options.textFilterguard deferred emission toontextso the filter could run there. But for an empty elementhtmlparser2never callsontext, so the injected text was emitted by neither branch and was lost.Fix
Emit
frame.innerTextthroughoptions.textFilterhere when one is set, mirroring the existing discard path that already filtersframe.innerText:addedText = truekeepsontextfrom double-emitting on the non-empty path (wherehtmlparser2does fireontext), so output for the existing non-empty cases is unchanged.Tests
Added one test to
packages/sanitize-html/test/test.jsfor the empty-tag + identity-textFiltercase. It fails on the unpatched code ('<a></a>'instead of'<a>some new text</a>') and passes after the fix. The full package suite goes from 213 passing + 1 failing to 214 passing, with no regressions to the existing non-emptytransformTagstext +textFiltertest.Note: the standalone
apostrophecms/sanitize-htmlrepo was archived on 2026-02-26; this monorepo (packages/sanitize-html, v2.17.5) is the live home of the code.