@@ -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:" onmouseover="__xss=1" a="">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:" onclick="__xss=1" a="">click</a>' ,
315+ done
316+ ) ;
317+ } ) ;
318+
319+ it ( 'should block " (named) and " (hex) quote entities' , ( done ) => {
320+ runXssCase ( '<span style="x:" onmouseover="__xss=1" a="">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