Skip to content

fix(security): scan <style> CSS past a CDATA-hidden </style> - #1044

Merged
mrbobbytables merged 1 commit into
mainfrom
sec/style-cdata-scan
Oct 5, 2026
Merged

mrbobbytables merged 1 commit into
mainfrom
sec/style-cdata-scan

Conversation

@hivecommons-hive

Copy link
Copy Markdown
Contributor

Security Fix

findRemoteReferences() is an error-level gate in
scripts/validate-architecture-assets.mjs:268-270: an SVG published from
static/ is refused if it fetches a resource from a third-party host, because
that fetch discloses the visitor's IP address, User-Agent and Referer to a
host the diagram's author chose, from the site's own origin.

Its <style> arm read the CSS with one lazy
<style ...>([\s\S]*?)</style> regex. A browser parses a standalone .svg as
XML, where a </style> inside a CDATA section is character data and does
not close the element — but the lazy match stops there, so every declaration
after the decoy went unscanned, in a file that is well-formed and renders.

At main 900592b this well-formed SVG returned [] from both
findRemoteReferences and findActiveContent, so
npm run validate:architecture-assets accepted it:

<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
  <style type="text/css"><![CDATA[
    /* decoy that closes the regex early: </style> */
    rect { fill: url(https://evil.example/track.svg#g); }
  ]]></style>
  <rect width="100" height="100"/>
</svg>

What changed

scripts/lib/svg-active-content.mjs only. STYLE_BLOCK_PATTERN is replaced by
styleBlockContents(), which walks from the open tag and consumes CDATA
sections whole before < is given any meaning, so an inner </style> cannot
end the block. Two related behaviours fall out of the same walk:

  • An unclosed <style> is now read to the end of the document. It
    previously matched nothing and was not scanned at all; an HTML parser runs it
    to EOF and XML rejects the file outright, so reading the remainder is the
    conservative answer under either grammar.
  • A self-closing <style/> still swallows no content.

The close-tag test uses a sticky regex rather than re-slicing the source, so
the walk stays linear over the 2 MB asset ceiling the validator enforces.

Five regression tests in tests/svg-active-content.test.mjs cover the CDATA
decoy, a second block following a decoy, an unclosed block and <style/>; the
stale STYLE_BLOCK_PATTERN comment on the existing empty-block test is
updated.

Verification

  • npm run test:unit — 1916/1916 pass (1912 before, +4 net new)
  • npm run check — exit 0
  • npm run validate:architecture-assets — 172 real assets, no new finding
    (the one pre-existing foreignObject warning is unchanged)

Scope

Files: scripts/lib/svg-active-content.mjs, tests/svg-active-content.test.mjs.
Function cluster: STYLE_BLOCK_PATTERN → styleBlockContents /
findRemoteReferences.

Disjoint from the open held PRs: #1042 is test-only and covers
XML_DECLARATION_ENCODING and PROCESSING_INSTRUCTION in the same file —
different functions and different test blocks; #1039 is
scripts/lib/mdx-active-content.mjs; #1034 and #1040 are the e2e coverage
build.

Closes #1043


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

findRemoteReferences() is an error-level gate in
validate-architecture-assets.mjs: an SVG that fetches from a third-party host
is refused, because the fetch discloses the visitor's IP, User-Agent and
Referer from the site's own origin.

Its <style> arm read the CSS with one lazy regex. A browser parses a
standalone .svg as XML, where a </style> inside a CDATA section is character
data and does not close the element -- but the lazy match stops there, so
every declaration after the decoy went unscanned in a file that is well-formed
and renders. A rule such as rect{fill:url(https://host/x.svg#g)} placed after
it passed both gates.

Read the CSS with a scan that consumes CDATA sections whole before '<' is
given any meaning. An unclosed <style> now reads to the end of the document:
an HTML parser runs it to EOF and XML rejects the file outright, so scanning
the remainder is the conservative answer under either grammar. A self-closing
<style/> still swallows no content.

Closes #1043

Signed-off-by: sec-check <sec-check@hive.kubestellar.io>
@hivecommons-hive hivecommons-hive Bot added the hold label Oct 4, 2026
@hivecommons-hive

Copy link
Copy Markdown
Contributor Author

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 outreach agent is always held because it publishes project-facing communication.

Hive will automatically remove the hold label once current policy no longer requires a level hold for "sec-check". If this is an outreach PR, a human must review it and remove the label.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[sec-check] CDATA-hidden </style> hides remote CSS fetches from findRemoteReferences

1 participant