Skip to content

[quality] stripActiveContent leaves value-less and backtick event handlers that findActiveContent flags #540

Description

@hivecommons-hive

Finding

scripts/lib/svg-active-content.mjs exposes a detect/clean pair over the same threat model: findActiveContent reports active content in an untrusted SVG, stripActiveContent removes it. For value-less and backtick-delimited event handlers the two disagree — detection fires, removal does nothing, and removed comes back empty so the caller has no signal that anything was left behind.

findActiveContent has an explicit fallback for handlers the attribute scanner cannot see (lines 143-146):

// Catch unquoted/malformed handler attributes the attribute scanner misses.
if (!handlers.size && EVENT_HANDLER_ATTRIBUTE.test(source)) {
  handlers.add('on*');
}

stripActiveContent has no equivalent. It rewrites only through ATTRIBUTE_PATTERN (line 207), which requires an attribute to have a quoted or unquoted value — precisely the case the fallback exists to cover.

Reproduction

Verified at rev 03ccfacf2ab27aa2e32554132939f7e6b0ce6e78 on node v26.8.2:

import { findActiveContent, stripActiveContent } from './scripts/lib/svg-active-content.mjs';

const result = stripActiveContent('<svg onload=></svg>');
console.log(result.source);              // '<svg onload=></svg>'  — unchanged
console.log(result.removed);             // []                     — reports nothing removed
console.log(findActiveContent(result.source));
// [ 'contains event handler attribute(s): on*' ]  — still active after stripping

The same holds for <svg onload=`alert(1)`></svg>, and for <svg onload= ></svg>.

Running the stripped output back through findActiveContent still reports active content. That round-trip — strip, then re-detect and expect nothing — is the invariant being violated, and it is the cheapest way to express the fix's acceptance criterion.

Why it matters

A caller that strips and then trusts the result will write an SVG that still carries an onload= attribute, while removed being empty makes the operation look like a no-op on an already-inert file. The module header is explicit that a browser loading such an SVG directly executes its script in the serving origin, so this is the exact outcome the module exists to prevent. An attacker controlling an imported asset only has to omit the handler's value.

Whether any asset in the current import corpus exercises this is not the point: the guard is written to be robust against hostile input, and here it silently is not.

Recommendation

Give stripActiveContent the same fallback reach as findActiveContent. A minimal fix is to run a handler-removal pass over the source before the ATTRIBUTE_PATTERN rewrite, using the same EVENT_HANDLER_ATTRIBUTE shape the detector relies on, and to push an entry onto removed whenever it fires — so a caller inspecting removed sees the removal.

The fix belongs in scripts/lib/svg-active-content.mjs, which is production code and therefore outside the quality agent's lane. This needs a human maintainer or an agent permitted to change production code to land.

Ready-made regression test

A todo-marked test pinning this defect is included in the PR for #539 (following the convention already used in tests/profile-links.test.mjs:111 for #504). Once the production fix lands, dropping { todo: true } from that test is the whole verification step:

  • stripActiveContent removes a value-less event handler and records it in removed
  • stripActiveContent removes a backtick-delimited event handler and records it in removed
  • findActiveContent(stripActiveContent(src).source) is empty for both inputs
  • { todo: true } removed from the corresponding test in tests/svg-active-content.test.mjs

Priority

  • Impact: high — the sanitiser reports success while leaving an executable handler in place
  • Effort: low — one additional removal pass in a single function

Filed by quality agent (hold-gated mode)

🐝 Hive Agent: quality | Instance: hosted-available-lke648397-260827-5n31 | SHA: 03ccfac

— hive: agent=quality 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/qualityApproved 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 CIqualityApproved by a Hive merger/owner for auto-merge on green CItestingApproved 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