Repository navigation
fix(security): strip DOCTYPEs whose external identifier contains a bracket - #1024
hivecommons-hive[bot] wants to merge 1 commit into
Conversation
…acket
stripDoctype() read '[' as the unambiguous start of a DOCTYPE's internal
subset, but an ExternalID carries quoted literals that may contain '[', ']'
or '>'. A declaration such as
<!DOCTYPE svg SYSTEM "https://host.example/x[.dtd">
has no internal subset, yet the old pattern saw one that never closes. With
no later ']'-then-'>' the declaration was returned unchanged, so the strip
silently did nothing and an external DTD reference survived into published
content; with one -- every <![CDATA[...]]> section supplies it -- the lazy
subset match deleted every byte in between, destroying live markup. Both
outcomes are inert, so both passed the callers' post-strip rescans in
mirrorLandscapeLogo() and sanitizeArchitectureAssets(), neither of which has
a DOCTYPE gate of its own.
Match each part with a quote-aware alternation so a quoted literal always
consumes through its closing mate. This also fixes '>' and ']>' inside a
quoted system identifier, and preserves the documented behaviour for a subset
that genuinely never closes: it still matches nothing and is left for the
<!ENTITY> check to flag. The branches are disjoint on their first character,
so matching stays linear.
Make the auto-fix in validateSvg() fail closed as well, recording an error
rather than reporting 'removed DOCTYPE' for a declaration still present. That
also covers the early-return write, which persisted the result with no rescan.
Signed-off-by: sec-check <sec-check@hive.kubestellar.io>
|
Important Held for human review by the hive's ACMM level gate. This PR was opened by the "sec-check" agent while Hive policy required a human checkpoint for that agent. Non-outreach agents are held at ACMM L3–L5; the Hive will automatically remove the |
The alternation branches of DOCTYPE_PATTERN are disjoint on their first character, so a 200k-character unterminated literal cannot trigger combinatorial backtracking. Regression test adapted from PR cncf#1024. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Bob Killen <bkillen@linuxfoundation.org>
|
Superseded by #1025, which applies the same quote-aware DOCTYPE_PATTERN and fail-closed validateSvg() guard, and additionally gates the two publishing paths this PR's description notes have no DOCTYPE check of their own: mirrorLandscapeLogo() and sanitizeArchitectureAssets(). An unclosed subset without an <!ENTITY declaration passes the entity check, so those sites need their own hasDoctype() gate. #1025 also carries this PR's linear-time regression test. Thanks — the analysis in #1023 and here was spot on. |
…cf#1025) * fix(security): make DOCTYPE stripping quote-aware and fail closed stripDoctype() read a bare "[" as the start of a DOCTYPE's internal subset, but XML's doctypedecl grammar allows "[", "]" and ">" inside an ExternalID's quoted literals. A system identifier containing "[" therefore either matched nothing -- the declaration survived while validateSvg() reported "removed DOCTYPE" -- or, when an unrelated "]>" appeared later (every CDATA close supplies one), deleted all markup in between. Both outcomes passed the post-strip rescans because the result is inert, so the sanitizer certified bytes it never cleaned. Make DOCTYPE_PATTERN consume quoted literals through their closing mate before giving the structural characters any meaning. Branches are disjoint on their first character, so matching stays linear. A declaration whose subset or literal never closes still matches nothing and is left in place. That leftover is now a hard stop at every publishing call site instead of an assumed success: validateSvg() records an error rather than reporting a removal that did not happen (covering the quality:false early-return write, which has no rescan), mirrorLandscapeLogo() refuses the logo, and sanitizeArchitectureAssets() fails the import. Closes cncf#1023 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Bob Killen <bkillen@linuxfoundation.org> * test: pin linear-time matching of an unterminated DOCTYPE literal The alternation branches of DOCTYPE_PATTERN are disjoint on their first character, so a 200k-character unterminated literal cannot trigger combinatorial backtracking. Regression test adapted from PR cncf#1024. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Bob Killen <bkillen@linuxfoundation.org> --------- Signed-off-by: Bob Killen <bkillen@linuxfoundation.org> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Security Fix
stripDoctype()inscripts/lib/svg-active-content.mjsread[as theunambiguous start of a DOCTYPE's internal subset. The XML grammar does not
allow that: an
ExternalIDcarries quoted literals, which may themselvescontain
[,]or>. Sohas no internal subset at all, but the old pattern saw one that never closes.
That failed two different ways, and both survived the callers' post-strip
rescans because the result is inert either way:
]-then->- neither alternative matched, the declaration cameback unchanged, and an attacker-chosen external DTD reference survived into
published content while the run reported it removed.
]-then->- and every<![CDATA[...]]>section supplies one,which is routine in the draw.io and Illustrator exports these pipelines
mirror - the lazy subset match deleted every byte in between, taking the
<svg>root element with it.Neither
mirrorLandscapeLogo()(scripts/lib/enduser-collector.mjs) norsanitizeArchitectureAssets()(scripts/import-architectures.mjs) has aDOCTYPE gate of its own, so both published the file believing it was cleaned.
Changes
scripts/lib/svg-active-content.mjs- match each part of the declaration witha quote-aware alternation, so a quoted literal always consumes through its
closing mate. This also fixes
>and]>inside a quoted system identifier,which failed for the same reason. The documented behaviour for a subset that
genuinely never closes is preserved: it still matches nothing and is left in
place for the
<!ENTITYcheck to flag. The alternation branches are disjointon their first character, so matching stays linear - a regression test pins a
200k-character unterminated literal under a second (it completes in ~0ms).
scripts/validate-architecture-assets.mjs- make the auto-fix fail closed.It previously pushed
removed DOCTYPEwithout checking that the removalhappened, and for the five
quality: falseasset roots it wrote the result atthe early return with no rescan at all. It now records an error and leaves the
file alone when a DOCTYPE survives the strip, which also keeps any future gap
in the pattern from being reported as a completed fix.
Verification
validate-architecture-assets --fixexited 0 reportingremoved DOCTYPEfor two fixtures, leaving onewith its DOCTYPE intact and the other truncated to
</style><rect/></svg>.After the fix both are stripped correctly with their CDATA preserved.
tests/svg-active-content.test.mjsandtests/validate-architecture-assets.test.mjs.touched files).
npm run test:unit:coverage:checkpasses;npm run check:formatclean.validate-architecture-assetsover the real 172-asset corpus is unchanged -same single pre-existing
foreignObjectwarning, no new findings.Scope:
DOCTYPE_PATTERN/stripDoctypeinscripts/lib/svg-active-content.mjsand the DOCTYPE branch of
validateSvg()inscripts/validate-architecture-assets.mjs, plus their two test files. Nooverlap with #1018 (contributor template / PR template links).
Closes #1023
Filed by sec-check agent (ACMM L4/L5 - hold-gated mode). Hold-gated: human review required.
— hive: agent=sec-check backend=copilot model=claude-opus-5 copilot=1.0.88