diff --git a/tests/svg-active-content.test.mjs b/tests/svg-active-content.test.mjs index 7b852770..142dbaab 100644 --- a/tests/svg-active-content.test.mjs +++ b/tests/svg-active-content.test.mjs @@ -341,3 +341,62 @@ test('leaves foreignObject itself untouched', () => { assert.deepEqual(findActiveContent(svg), []); assert.deepEqual(stripActiveContent(svg), { source: svg, removed: [] }); }); + +// ATTRIBUTE_PATTERN accepts three value forms -- double-quoted, single-quoted +// and unquoted -- and findActiveContent/stripActiveContent read them through +// `match[2] ?? match[3] ?? match[4]` and `dq ?? sq ?? uq`. Every case above +// writes double-quoted markup, so only the first alternative of either +// fallback chain was ever exercised: a regression that dropped the +// single-quoted or unquoted alternative from the pattern, or read the wrong +// capture group, would have left the suite green while every +// `onclick='alert(1)'` and `href=javascript:alert(1)` an SVG editor emits +// passed the gate unseen. Browsers accept all three forms identically. +const QUOTING_VARIANTS = [ + { article: 'a', label: 'single-quoted', quote: "'" }, + { article: 'an', label: 'unquoted', quote: '' }, +]; + +for (const { article, label, quote } of QUOTING_VARIANTS) { + test(`detects and removes ${article} ${label} event handler attribute`, () => { + const svg = ``; + assert.deepEqual(findActiveContent(svg), [ + 'contains event handler attribute(s): onclick', + ]); + + const { source, removed } = stripActiveContent(svg); + assert.doesNotMatch(source, /onclick/i); + assert.doesNotMatch(source, /alert\(1\)/); + assert.ok(removed.includes('onclick attribute')); + assert.deepEqual(findActiveContent(source), []); + }); + + test(`detects and removes ${article} ${label} script URI`, () => { + const svg = ``; + assert.deepEqual(findActiveContent(svg), [ + 'contains a script URI in href="javascript:..."', + ]); + + const { source, removed } = stripActiveContent(svg); + assert.doesNotMatch(source, /javascript:/i); + assert.ok(removed.includes('href attribute (javascript:)')); + assert.deepEqual(findActiveContent(source), []); + }); + + test(`detects and removes ${article} ${label} embedded document attribute`, () => { + const svg = ``; + assert.deepEqual(findActiveContent(svg), [ + 'contains an embedded document attribute: srcdoc (carries markup that executes in this origin)', + ]); + + const { source, removed } = stripActiveContent(svg); + assert.doesNotMatch(source, /srcdoc/i); + assert.ok(removed.includes('srcdoc attribute (embedded document)')); + assert.deepEqual(findActiveContent(source), []); + }); + + test(`reads ${article} ${label} value without disturbing inert markup`, () => { + const svg = ``; + assert.deepEqual(findActiveContent(svg), []); + assert.deepEqual(stripActiveContent(svg), { source: svg, removed: [] }); + }); +}