Skip to content

[sec-check] SVG sanitizer fails open on namespace-prefixed script/handler elements and on foreignObject srcdoc iframes #587

Description

@hivecommons-hive

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="&lt;script&gt;alert(1)&lt;/script&gt;"></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:

  1. 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.
  2. Add iframe, embed, and object to ACTIVE_ELEMENTS — none of them are
    legitimate in a static diagram asset. Leave foreignObject allowed.
  3. 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

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent/securityApproved by a Hive merger/owner for auto-merge on green CIhive/hosted-available-lke648397-260827-5n31Approved by a Hive merger/owner for auto-merge on green CIsecurityApproved by a Hive merger/owner for auto-merge on green CI

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions