Skip to content

Commit 47763c2

Browse files
committed
Add tests
1 parent 6dfed31 commit 47763c2

1 file changed

Lines changed: 49 additions & 1 deletion

File tree

test/jasmine/tests/toimage_test.js

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ describe('Plotly.toImage', function () {
213213
})
214214
.then(function (d) {
215215
expect(d.indexOf('data:image/')).toBe(-1);
216-
expect(d.length).toBeWithin(32062, 1e3, 'svg image length');
216+
expect(d.length).toBeWithin(32520, 1e3, 'svg image length');
217217
})
218218
.then(function () {
219219
return Plotly.toImage(gd, { format: 'webp', imageDataOnly: true });
@@ -273,6 +273,54 @@ describe('Plotly.toImage', function () {
273273
.then(done, done.fail);
274274
});
275275

276+
describe('SVG export attribute escaping (XSS regression)', () => {
277+
// Regression: pseudo-html style attributes encoded with numeric quote
278+
// entities used to break out of the serialized SVG attribute context
279+
// because htmlEntityDecode() ran after XMLSerializer and un-escaped
280+
// ". See src/snapshot/tosvg.js htmlEntityDecode.
281+
const parser = new DOMParser();
282+
283+
const expectNoEventHandlerAttrs = (svg) => {
284+
const doc = parser.parseFromString(svg, 'image/svg+xml');
285+
const nodes = doc.getElementsByTagName('*');
286+
for (const el of nodes) {
287+
for (const attr of el.attributes) {
288+
const name = attr.name.toLowerCase();
289+
if (name.startsWith('on')) {
290+
fail(`parsed SVG has event-handler attribute <${el.nodeName} ${name}="${attr.value}">`);
291+
}
292+
}
293+
}
294+
};
295+
296+
const runXssCase = (payload, done) => {
297+
const fig = {
298+
data: [{ x: [1], y: [1], type: 'scatter' }],
299+
layout: { annotations: [{ x: 1, y: 1, showarrow: false, text: payload }] }
300+
};
301+
302+
Plotly.newPlot(gd, fig)
303+
.then(() => Plotly.toImage(gd, { format: 'svg', imageDataOnly: true }))
304+
.then((svg) => expectNoEventHandlerAttrs(decodeURIComponent(svg)))
305+
.then(done, done.fail);
306+
};
307+
308+
it('should not let <span style=...> entity-encoded quotes escape attribute context', (done) => {
309+
runXssCase('<span style="x:&#34; onmouseover=&#34;__xss=1&#34; a=&#34;">hi</span>', done);
310+
});
311+
312+
it('should not let <a href=... style=...> entity-encoded quotes escape attribute context', (done) => {
313+
runXssCase(
314+
'<a href="https://example.com" style="x:&#34; onclick=&#34;__xss=1&#34; a=&#34;">click</a>',
315+
done
316+
);
317+
});
318+
319+
it('should block &quot; (named) and &#x22; (hex) quote entities', (done) => {
320+
runXssCase('<span style="x:&quot; onmouseover=&#x22;__xss=1&#x22; a=&quot;">hi</span>', done);
321+
});
322+
});
323+
276324
it('should work on pages with <base>', function (done) {
277325
var parser = new DOMParser();
278326

0 commit comments

Comments
 (0)