Repository navigation
fix: re-scan the SVG stripActiveContent returns - #1516
Conversation
|
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 keep the |
✅ Deploy Preview for endusers ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
stripActiveContent() documents that it re-scans its result and throws if active content survives, and two mirroring callers write that result into static/ with no active-content check of their own. The re-scan was never implemented. Add it, and cover the post-condition for every strippable construct. Signed-off-by: sec-check <sec-check@hive.kubestellar.io>
The gate requires 100% source regions, and the re-scan throw is unreachable
with the default detector because it and the stripper agree by construction.
stripActiveContent() now accepts { scan } (default findActiveContent); the
test drives a detector that flags the rebuilt bytes and asserts the string
never reaches the caller. Production callers are unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: mrbobbytables <mrbobbytables@users.noreply.github.com>
fe0a884 to
46b50d7
Compare
mrbobbytables
left a comment
There was a problem hiding this comment.
Added a scan seam so the survival throw is covered; enqueuing.
🔒 Hold-gate integrity: branch changed while hold-gated — fresh review requiredThis PR's branch moved while it sat hold-gated: the head recorded when the hold was applied was Commits not present in the hold-time snapshot (2):
Authors not in the hold-time author set: A human should review the FULL diff at the current head — a rebase or force-push renames every commit, so everything above needs eyes even if it looks familiar. Removing the hold label after that review is the fresh approval: the guard has re-pinned its snapshot to the current head, so a clean lift re-opens the merge lanes. |
There was a problem hiding this comment.
Read the diff against the tree at 46b50d7, including both mirroring callers. This looks correct to me.
Verified:
- The claimed gap was real: before this change,
stripActiveContent()ranfindActiveContent()/structuralFindings()only on the input and returnedrebuild(text, parsed.events, { strip: true })unexamined, despite its JSDoc promising a re-scan. - Both callers the body names are as described:
scripts/lib/architecture-content.mjs:268andscripts/lib/enduser-collector.mjs:164write the returned string intostatic/gated only byfindRemoteReferences(), so this function is the sole active-content gate on those publish paths.scripts/import-architectures.mjs:253explicitly assumes "stripActiveContent() verifies the string it returns" — this PR makes that comment true. - The new re-scan (
scripts/lib/svg-active-content.mjs:809-814) fails closed by throwing, which both callers already handle (architecture-content.mjs wraps the mirror in try/catch and warns; enduser-collector's caller likewise). - Ran
node --test tests/svg-active-content.test.mjsat the PR head: 149/149 pass, including the 17 new post-condition tests.
One observation (info, not a defect): the scan injection seam exists only so the throw is reachable in tests, since the default detector and stripper agree by construction. No production caller passes options, so it can't weaken the gate; the JSDoc says this plainly. Fine as-is.
Note the early return at svg-active-content.mjs:786 (input with zero findings) skips the re-scan — trivially satisfied, since the same detector just reported the input clean.
Hold-gated per the PR body; a maintainer still needs to sign off, but I found nothing blocking.
— hive: agent=reviewer backend=copilot model=claude-fable-5 copilot=1.0.88
Security Fix
Claims exactly two files:
scripts/lib/svg-active-content.mjs(
stripActiveContent) andtests/svg-active-content.test.mjs.stripActiveContent()is the sanitizer that makes a third-party SVG safe topublish from
static/. Its JSDoc states that "the result is re-scanned beforeit is returned" and that it throws if "active content survives". Neither was
implemented: the only verification in the function,
structuralFindings(),runs on the input before the rebuild, and
rebuild(..., { strip: true })was returned unexamined.
That post-condition is load-bearing.
architecture-content.mjs:268(
mirrorArtworkUrls, reached from a community-submitted reference-architectureissue) and
enduser-collector.mjs:164(mirrorLandscapeLogo) both write thereturned string into
static/and gate the write onfindRemoteReferences()only — so for those two paths
stripActiveContent()is the soleactive-content gate on bytes published at the site origin, where a
directly-opened SVG executes script in that origin.
import-architectures.mjs:253re-scans, but its comment says it does so forits own later text edit and names the premise outright: "stripActiveContent()
verifies the string it returns."
Not exploitable at
1ae3403: findings and removals both come fromclassifyTag()over the same parse and the output is serialized from thattree, so they agree by construction. This change makes that agreement a
property of the returned bytes rather than an argument about two code paths —
the file's eight
fix(security)commits (#1019 #1025 #1044 #1068 #1090 #1168#1176 #1245) are the record of that argument being revised.
scripts/lib/svg-active-content.mjs: after the rebuild, runfindActiveContent()over the serialized output and throw when anythingactive survives, so a divergence between the removal set, the serialization
and the detector fails closed instead of publishing a document that only
looks sanitized.
tests/svg-active-content.test.mjs: 15 new tests asserting thepost-condition on the returned bytes for every construct the module strips
(each
ACTIVE_ELEMENTSmember, event-handler attributes,srcdoc, ajavascript:href, a namespace-prefixedscript,animate/setinstallingan href), plus one fixture combining all of them with an
<?xml-stylesheet?>processing instruction. The existing test "throws rather than returning a
source the detector still flags" only covers the structural refusal, which
happens before the rebuild; this covers the other half of the same promise.
Full unit suite green: 2711/2711 (
npm run test:unit).npm run check:formatclean.
Closes #1515
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