Security Finding
Severity: high
Type: unsafe-pattern (stored XSS / sanitizer bypass)
scripts/lib/svg-active-content.mjs is the only barrier between third-party SVG
assets and the site origin. Both of its entry points fail open on two input
shapes:
findActiveContent() — the build gate used by scripts/validate-architecture-assets.mjs
stripActiveContent() — the sanitizer used by scripts/import-architectures.mjs
for both mirrored artwork (line 247) and imported architecture assets (line 308)
1. XML-namespace-prefixed active elements bypass detection and removal
ACTIVE_ELEMENT_PATTERN (and the paired/standalone removal regexes in
stripActiveContent) anchor on <\s*(script|handler|listener)\b, so any
namespace prefix defeats them. SVG assets under static/img/ are served
standalone as image/svg+xml and parsed as XML, where the prefix is arbitrary
and only the bound namespace URI matters — <x:script xmlns:x="http://www.w3.org/2000/svg">
is a script element and executes.
Reproduced against scripts/lib/svg-active-content.mjs at a994a2e:
### namespaced svg:script
findActiveContent: NONE (PASSES VALIDATION)
stripped output : <svg xmlns:svg="http://www.w3.org/2000/svg"><svg:script>alert(1)</svg:script></svg>
### arbitrary prefix script
findActiveContent: NONE (PASSES VALIDATION)
stripped output : <svg xmlns:x="http://www.w3.org/2000/svg"><x:script>alert(1)</x:script></svg>
### namespaced handler
findActiveContent: NONE (PASSES VALIDATION)
stripped output : <svg xmlns:ev="http://www.w3.org/2001/xml-events"><svg:handler ev:event="load">alert(1)</svg:handler></svg>
The same anchoring bug affects ANIMATED_URI_ELEMENT (<svg:animate attributeName="href">).
2. srcdoc and HTML embedding elements inside <foreignObject>
<foreignObject> hosts XHTML, and an <iframe srcdoc="..."> carries its payload
as an entity-encoded attribute value that no scheme scan inspects. <embed> and
<object> load external documents the same way.
### foreignObject iframe srcdoc
findActiveContent: NONE (PASSES VALIDATION)
stripped output : <svg xmlns="http://www.w3.org/2000/svg"><foreignObject><iframe srcdoc="<script>alert(1)</script>"></iframe></foreignObject></svg>
Note <foreignObject> itself is legitimate and in use today
(static/img/architectures/swisscom-cloud-native-telco/swisscom-cloud-native-telco-automation-layers.svg),
so it must stay allowed — only the embedding elements and srcdoc are the finding.
Impact
npm run import:architectures runs unattended on a daily schedule
(.github/workflows/import-architectures.yml) against cncf/architecture, a
repository this project does not control and which accepts community pull
requests. An SVG committed there in either shape is copied into
static/img/architectures/<id>/, passes validate:architecture-assets in CI,
and is published. 49 SVGs are already served from static/img/.
A visitor who opens the asset URL directly (or follows a link to it) executes
attacker JavaScript in the site's origin: defacement of same-origin pages,
localStorage/cookie access, and a credible phishing surface on a CNCF-branded
domain. The daily import also opens a PR automatically, so the malicious bytes
arrive with a routine, low-scrutiny diff.
I re-scanned all 49 SVGs currently in static/img/ for both shapes: the only
match is the legitimate <foreignObject> above. Nothing in the repo is
currently exploited — this is a gate that would not stop the next one.
Recommendation
In scripts/lib/svg-active-content.mjs:
- Allow an optional XML namespace prefix everywhere an active element is
matched. A shared fragment such as (?:[a-z_][-a-z0-9_.]*:)? applied to
ACTIVE_ELEMENT_PATTERN, ANIMATED_URI_ELEMENT, the paired/standalone
removal patterns in stripActiveContent(), and the orphaned-closing-tag
cleanup.
- Add
iframe, embed, and object to ACTIVE_ELEMENTS — none of them are
legitimate in a static diagram asset. Leave foreignObject allowed.
- Treat an attribute named
srcdoc as active in both findActiveContent()
and stripActiveContent(), independent of its value.
Tests belong in tests/svg-active-content.test.mjs, which already covers the
unprefixed cases.
A fix PR follows and will reference this issue.
🐝 Hive Agent: security | Instance: hosted-available-lke648397-260827-5n31 | SHA: a994a2e
— hive: agent=sec-check backend=copilot model=claude-opus-5 copilot=1.0.88
Security Finding
Severity: high
Type: unsafe-pattern (stored XSS / sanitizer bypass)
scripts/lib/svg-active-content.mjsis the only barrier between third-party SVGassets and the site origin. Both of its entry points fail open on two input
shapes:
findActiveContent()— the build gate used byscripts/validate-architecture-assets.mjsstripActiveContent()— the sanitizer used byscripts/import-architectures.mjsfor both mirrored artwork (line 247) and imported architecture assets (line 308)
1. XML-namespace-prefixed active elements bypass detection and removal
ACTIVE_ELEMENT_PATTERN(and the paired/standalone removal regexes instripActiveContent) anchor on<\s*(script|handler|listener)\b, so anynamespace prefix defeats them. SVG assets under
static/img/are servedstandalone as
image/svg+xmland parsed as XML, where the prefix is arbitraryand only the bound namespace URI matters —
<x:script xmlns:x="http://www.w3.org/2000/svg">is a script element and executes.
Reproduced against
scripts/lib/svg-active-content.mjsata994a2e:The same anchoring bug affects
ANIMATED_URI_ELEMENT(<svg:animate attributeName="href">).2.
srcdocand HTML embedding elements inside<foreignObject><foreignObject>hosts XHTML, and an<iframe srcdoc="...">carries its payloadas an entity-encoded attribute value that no scheme scan inspects.
<embed>and<object>load external documents the same way.Note
<foreignObject>itself is legitimate and in use today(
static/img/architectures/swisscom-cloud-native-telco/swisscom-cloud-native-telco-automation-layers.svg),so it must stay allowed — only the embedding elements and
srcdocare the finding.Impact
npm run import:architecturesruns unattended on a daily schedule(
.github/workflows/import-architectures.yml) againstcncf/architecture, arepository this project does not control and which accepts community pull
requests. An SVG committed there in either shape is copied into
static/img/architectures/<id>/, passesvalidate:architecture-assetsin CI,and is published. 49 SVGs are already served from
static/img/.A visitor who opens the asset URL directly (or follows a link to it) executes
attacker JavaScript in the site's origin: defacement of same-origin pages,
localStorage/cookie access, and a credible phishing surface on a CNCF-brandeddomain. The daily import also opens a PR automatically, so the malicious bytes
arrive with a routine, low-scrutiny diff.
I re-scanned all 49 SVGs currently in
static/img/for both shapes: the onlymatch is the legitimate
<foreignObject>above. Nothing in the repo iscurrently exploited — this is a gate that would not stop the next one.
Recommendation
In
scripts/lib/svg-active-content.mjs:matched. A shared fragment such as
(?:[a-z_][-a-z0-9_.]*:)?applied toACTIVE_ELEMENT_PATTERN,ANIMATED_URI_ELEMENT, the paired/standaloneremoval patterns in
stripActiveContent(), and the orphaned-closing-tagcleanup.
iframe,embed, andobjecttoACTIVE_ELEMENTS— none of them arelegitimate in a static diagram asset. Leave
foreignObjectallowed.srcdocas active in bothfindActiveContent()and
stripActiveContent(), independent of its value.Tests belong in
tests/svg-active-content.test.mjs, which already covers theunprefixed cases.
A fix PR follows and will reference this issue.
🐝 Hive Agent:
security| Instance:hosted-available-lke648397-260827-5n31| SHA:a994a2e— hive: agent=sec-check backend=copilot model=claude-opus-5 copilot=1.0.88