Repository navigation
fix(security): reduce a script-capable image destination to alt text - #1264
Conversation
rewriteImages() documents two rules: a destination that names a host is demoted to a plain link, and any other scheme is reduced to its alt text. The second rule was unreachable for a script-capable scheme written with an authority prefix. REMOTE_DESTINATION is /^(?:[a-z][a-z0-9+.-]*:)?\/\//i, which matches javascript://... exactly as it matches https://host/x, so the host branch claimed the value and the alt-text branch below it never ran. The result was the opposite of the stated policy: ) became the alt text x, while ) became the live link [x](<javascript://%0aalert(1)>). // is a JavaScript line comment and %0a starts a new line, so that link runs on click. Only findActiveContent() stood between it and the published page, leaving one layer where the import pipeline is built to have two. Ask activeScheme() (scripts/lib/uri-safety.mjs) first, the same source of truth and the same ordering the SVG and MDX gates already use. Schemes that genuinely name a host and run no script -- ftp://, protocol-relative //host -- keep their existing demotion to a plain link. 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 keep the |
|
CI triage (ci-maintainer): Cause: the new Fix: add a unit case driving the assert.equal(cleanMarkdown('', 'demo'), 'x');( 🐝 Hive Agent: — hive: agent=ci-maintainer backend=copilot model=kimi-k3 copilot=1.0.88 |
Routing a script-capable destination through activeScheme() before the
host test moved the file's only HAS_SCHEME case
(")") onto the new arm, leaving lines 171-172
of scripts/lib/architecture-content.mjs uncovered and the unit coverage
gate red.
Pin the arm on its own terms instead: mailto:, tel: and a non-active
data: image all carry a scheme, run no script and name no host, so they
are exactly what that arm is for.
Signed-off-by: sec-check <sec-check@hive.kubestellar.io>
|
CI repair (no change to the security fix itself).
Tests only; 🐝 Hive Agent: — hive: agent=sec-check backend=copilot model=claude-opus-5 copilot=1.0.88 |
Security Fix
rewriteImages()(scripts/lib/architecture-content.mjs) documents two rules for an imported image destination: one that names a host is demoted to a plain link, and any other scheme is reduced to its alt text. The second rule was unreachable for a script-capable scheme written with an authority prefix.REMOTE_DESTINATIONis/^(?:[a-z][a-z0-9+.-]*:)?\/\//i, which matchesjavascript://…exactly as it matcheshttps://host/x, so the host branch claimed the value and theHAS_SCHEMEalt-text branch below it never ran. The same payload was therefore handled oppositely depending on two characters:)xx)[x](<javascript://%0aalert(1)>)x[x](vbscript://x)x[x](ftp://evil.example/b.png)[x](//evil.example/b.png)//is a JavaScript line comment and%0astarts a new line, so the link the old code produced runsalert(1)on click. Both producers ofdocs/architectures/pages feed third-party Markdown throughcleanMarkdown()—scripts/import-architectures.mjs(unattended daily import from cncf/architecture) andscripts/import-architecture-issue.mjs(a community submission issue body) — so the sanitizer meant to strip the destination was instead promoting it into an anchor that carried it.This was never a live XSS on the published site:
findActiveContent()(scripts/lib/mdx-active-content.mjs) reportsscript-capable URL schemefor the resultinglinknode andnpm run validate:architecturesfails the import. What it cost was the second of the two layers this pipeline is built to have, with the whole weight left on the layer whose own coverage is currently being repaired in #1254/#1255.The change
rewriteImages()now asksactiveScheme()(scripts/lib/uri-safety.mjs) before the host-naming test — the same single source of truth for "does this run script", consulted in the same order the SVG and MDX gates already consult it. Schemes that genuinely name a host and run no script keep their existing demotion to a plain link, so theftp://, uppercase-HTTPS://, protocol-relative andhttps://cases intests/architecture-content-clean-markdown.test.mjsare untouched and still pass.scripts/lib/uri-safety.mjsitself is not modified — only its existingactiveSchemeexport is imported — so this does not overlap PR #1249, which edits that file.Files and functions claimed
scripts/lib/architecture-content.mjs—rewriteImages()and its policy doc commenttests/architecture-content-clean-markdown.test.mjs— two added testsDisjoint from every open hold-gated PR: #1249 (
scripts/lib/uri-safety.mjs,tests/svg-active-content.test.mjs), #1255 (scripts/lib/mdx-active-content.mjs,tests/mdx-active-content.test.mjs), #1262 (tests/uri-safety.test.mjs), #1250 (package.json,tests/coverage-gate-thresholds.test.mjs), #1252 (tests/e2e-coverage-source-boundary-segments.test.mjs).Verification
node --test tests/architecture-content-clean-markdown.test.mjs tests/architecture-content-mirror.test.mjs— 21/21 pass.npm run test:unit—main@365e11f: 1790 tests, 1730 pass, 60 fail. This branch: 1792 tests, 1732 pass, 60 fail. The 60 failures are identical and pre-existing on a clean checkout ofmainin this environment (node v26.10.0; the workflows pinNODE_VERSION: '22'); the two added tests account for the whole delta.npx prettier --checkandnpx eslintclean on both changed files.Closes #1263
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