Skip to content

Filter invisible Unicode after HTML entity normalization - #3110

Merged
SamMorrowDrums merged 3 commits into
mainfrom
sammorrowdrums-filter-invisible-unicode-after-html-enti
Aug 19, 2026
Merged

Filter invisible Unicode after HTML entity normalization#3110
SamMorrowDrums merged 3 commits into
mainfrom
sammorrowdrums-filter-invisible-unicode-after-html-enti

Conversation

@SamMorrowDrums

Copy link
Copy Markdown
Collaborator

Problem

FilterInvisibleCharacters only ran before FilterHTMLTags in Sanitize. FilterHTMLTags (bluemonday) decodes HTML character entities, so a numeric entity like ​ or ​ becomes a literal U+200B (zero width space) after the invisible-character pass already ran — letting encoded invisible/bidi characters survive sanitization untouched.

The removal set also omitted a couple of relevant format/variation-selector ranges.

Fix

  • Sanitize now applies FilterInvisibleCharacters both before and after FilterHTMLTags/FilterCodeFenceMetadata: once up front (so raw invisible characters don't interfere with code-fence parsing, preserving existing behavior), and once more on the fully normalized output so entity-decoded characters can't escape the policy.
  • Expanded shouldRemoveRune coverage:
    • U+061C ARABIC LETTER MARK (a directional format character alongside the already-covered LRM/RLM).
    • U+FE00U+FE0F Variation Selectors.
    • U+E0100U+E01EF Variation Selectors Supplement.

These additions target characters commonly used for invisible-payload smuggling while leaving ordinary text, emoji, and CJK content untouched.

Testing

  • Added table-driven cases for decimal/hex HTML entities, direct runes, the newly-covered format/variation-selector ranges, and benign Unicode (verifying no legitimate text is damaged).
  • script/lint: 0 issues.
  • script/test: full suite passes.

Fixes #3101

FilterInvisibleCharacters previously ran only before FilterHTMLTags,
so numeric HTML entities (e.g. ​ or ​) that bluemonday
decodes into invisible or bidirectional control characters could
survive sanitization untouched. Sanitize now applies the
invisible-character filter both before HTML processing (so raw
invisible characters don't interfere with code-fence parsing) and
again after, so entity-decoded characters cannot escape the policy.

Also expands the removal set to include:
- ARABIC LETTER MARK (U+061C), a directional format character in the
  same family as the already-covered LRM/RLM marks.
- Variation selectors (U+FE00-U+FE0F) and the variation selectors
  supplement (U+E0100-U+E01EF), which can be used to hide payloads
  after emoji or other base characters.

Fixes #3101
@SamMorrowDrums
SamMorrowDrums requested a review from a team as a code owner August 19, 2026 12:17
Copilot AI balanced review requested due to automatic review settings August 19, 2026 12:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds post-normalization invisible-Unicode filtering to prevent HTML entities bypassing sanitization.

Changes:

  • Filters invisible characters before and after HTML processing.
  • Expands directional-mark and variation-selector coverage.
  • Adds regression and Unicode-range tests.
Show a summary per file
File Description
pkg/sanitize/sanitize.go Updates sanitization order and removal ranges.
pkg/sanitize/sanitize_test.go Adds entity-decoding and Unicode coverage.

Review details

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

  • Files reviewed: 2/2 changed files
  • Comments generated: 4
  • Review effort level: Balanced

Comment thread pkg/sanitize/sanitize.go Outdated
Comment thread pkg/sanitize/sanitize.go Outdated
Comment thread pkg/sanitize/sanitize.go Outdated
Comment thread pkg/sanitize/sanitize_test.go Outdated
SamMorrowDrums and others added 2 commits August 19, 2026 14:48
Address review feedback on the post-HTML-entity sanitization pass.

Entity decoding could still smuggle code-fence metadata past the
sanitizer. A first line such as "`​``steal secrets" is not a fence
in the raw input, so FilterCodeFenceMetadata left it alone; decoding the
entity and stripping the zero width space then produced a real fence with
its info string intact. Sanitize now re-runs the fence filter after the
input is fully normalized.

Filtering every variation selector also corrupted legitimate text: VS15
and VS16 select text or emoji presentation, so "✈️" was reduced to "✈",
and the Variation Selectors Supplement encodes registered CJK ideographic
variation sequences. Selectors are now filtered contextually. A selector
is kept when it can apply to the character it follows, and dropped when it
is orphaned, follows a removed or non-graphic character, or continues a
run of selectors. Supplement selectors additionally require a CJK
ideograph base, matching the Ideographic Variation Database. That keeps
the anti-smuggling property, since hidden payloads rely on selector runs,
without rewriting valid Unicode.

Also corrects a lowercase-hex test case that claimed uppercase digits.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@SamMorrowDrums
SamMorrowDrums merged commit 53fc915 into main Aug 19, 2026
19 checks passed
@SamMorrowDrums
SamMorrowDrums deleted the sammorrowdrums-filter-invisible-unicode-after-html-enti branch August 19, 2026 13:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Filter invisible Unicode after HTML entity normalization

2 participants