size, low, high, and optimum were missing from PURIFY_CONFIG.ALLOWED_ATTR
even though <select> and <meter> are already in ALLOWED_TAGS, so
<select size="4"> rendered at default height and <meter low/high/optimum>
lost its threshold-based gauge colouring. All four are pure
presentation/semantic attributes with no URL/script/event-handler
surface, so no security implication.
Also regenerates the pinned pre-Task-25 output fixture: its source
(html-block-test-body.html) already exercises size/low/high/optimum, so
the byte-identity test's expected output legitimately changes to include
them; verified the regenerated fixture's only diff from the prior one is
those four attributes now surviving.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four issues from adversarial review of the block-scoped <style> feature:
1. (Critical) transformBlock() recursed once per @media/@supports/@container
nesting level with no cap -- ~7000 nested rules blew the call stack, and
nothing between a Custom HTML block's toHtml() and the publish pipeline
catches exceptions, so this took down the whole page's publish and
crashed the live editor on every keystroke. Added MAX_NESTING_DEPTH=20
(pass the body through unscoped beyond it) and wrapped scopeCss() so it
never throws on any input, matching repairOrphanNodes's existing
contract. Caught and fixed a variable-shadowing bug in my own first pass
at this: the new depth parameter was silently shadowed by a pre-existing
`let depth` used for brace-matching in the same block, which would have
defeated the cap with no type error.
2. (Important) FORCE_BODY: true was unconditional, but it isn't a no-op for
style-free input: it also changes how the parser preserves whitespace
after a LEADING html comment, which this repo's own fixture starts with.
Verified via a raw byte-diff against HtmlBlock.tsx@6a9b227 (extracted
verbatim, run standalone against real dompurify+jsdom) that the fixture
gained bytes. Fixed by applying FORCE_BODY only when the input has a
real (non-comment) <style> tag to rescue -- confirmed empirically that
this is a true no-op for every other input. Pinned the old output as a
checked-in regression fixture and added a raw toBe() diff test.
3. (Important) scopeStyleBlocks() wasn't idempotent -- pasting previously
published/exported output into a fresh block nested a second wrapper
and re-prefixed every selector. Added isAlreadyScoped(), which detects
a lone root wrapper whose <style> content is already a no-op under
scopeCss for that wrapper's own class (reusing scopeCss's own
idempotency guarantee) and leaves it untouched.
4. (Minor) Documented, not fixed: the 32-bit scope-id hash is
brute-forceable (CSS-only impact, same trust tier as other accepted
risks here), and DOMPurify's SAFE_FOR_XML silently drops an entire
<style> block when its content merely looks tag-like (e.g.
content: "<Read More>").
1155/1155 tests passing (was 1141), tsc clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A customer's broad HTML fixture showed 38% of it silently deleted by
the shipped DOMPurify config: colspan/rowspan/scope, <dl>, <sub>/<sup>,
<details>/<summary>, inline <svg>, <video>/<audio>, lang/dir/role, and
<ol start/reversed> were all stripped. The site owner's call: be
generous, this block is an explicit escape hatch, allow forms too.
Widens PURIFY_CONFIG in HtmlBlock.tsx (45->119 tags, 16->108 attrs;
form/input/button/select/textarea removed from FORBID_TAGS) while
keeping the four non-negotiables intact: no <script>, no on*, no
javascript: URLs, iframes stay sandboxed. <style> stays blocked
(separate task adds scoped support later), including inside the newly
allowed inline SVG. SVG support is an explicit tag list mirroring
DOMPurify's own SVG vocabulary rather than USE_PROFILES, which turned
out to silently discard ALLOWED_ATTR entirely and pull in unaudited
tags (dialog, template, marquee, ...) not in scope here.
Fixture survival goes from 61.6% (9,739/15,815 bytes) to 94.2%
(14,899/15,815 bytes). Adds a fixture-driven regression + security
test file (HtmlBlock.security.test.ts) plus a checked-in copy of the
reference fixture, loaded via Vite's ?raw import so tests need no new
dependencies and can't silently drift from the thing being tested.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>