diff --git a/src/lib/svg_text_utils.js b/src/lib/svg_text_utils.js index 922c4b0213e..fe995cc74f7 100644 --- a/src/lib/svg_text_utils.js +++ b/src/lib/svg_text_utils.js @@ -242,6 +242,15 @@ util.plainText = function(_str) { return (_str || '').replace(STRIP_TAGS, ' '); }; +function encodeForHTML(_str) { + return (_str || '').replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(/\//g, '/'); +} + function convertToSVG(_str) { var htmlEntitiesDecoded = Plotly.util.html_entity_decode(_str); var result = htmlEntitiesDecoded @@ -270,15 +279,14 @@ function convertToSVG(_str) { // remove quotes, leading '=', replace '&' with '&' var href = extra.substr(4) .replace(/["']/g, '') - .replace(/=/, '') - .replace(/&/g, '&'); + .replace(/=/, ''); // check protocol var dummyAnchor = document.createElement('a'); dummyAnchor.href = href; if(PROTOCOLS.indexOf(dummyAnchor.protocol) === -1) return ''; - return ''; + return ''; } } else if(tag === 'br') return '
'; @@ -302,7 +310,7 @@ function convertToSVG(_str) { // most of the svg css users will care about is just like html, // but font color is different. Let our users ignore this. extraStyle = extraStyle[1].replace(/(^|;)\s*color:/, '$1 fill:'); - style = (style ? style + ';' : '') + extraStyle; + style = (style ? style + ';' : '') + encodeForHTML(extraStyle); } return tspanStart + (style ? ' style="' + style + '"' : '') + '>'; diff --git a/test/jasmine/tests/svg_text_utils_test.js b/test/jasmine/tests/svg_text_utils_test.js index 6d11560a105..be4601743c8 100644 --- a/test/jasmine/tests/svg_text_utils_test.js +++ b/test/jasmine/tests/svg_text_utils_test.js @@ -25,6 +25,11 @@ describe('svg+text utils', function() { expect(a.attr('xlink:show')).toBe(href === null ? null : 'new'); } + function assertTspanStyle(node, style) { + var tspan = node.select('tspan'); + expect(tspan.attr('style')).toBe(style); + } + function assertAnchorAttrs(node) { var a = node.select('a'); @@ -75,6 +80,16 @@ describe('svg+text utils', function() { assertAnchorLink(node, null); }); + it('whitelist relative hrefs (interpreted as http)', function() { + var node = mockTextSVGElement( + '
mylink' + ); + + expect(node.text()).toEqual('mylink'); + assertAnchorAttrs(node); + assertAnchorLink(node, '/mylink'); + }); + it('whitelist http hrefs', function() { var node = mockTextSVGElement( 'bl.ocks.org' @@ -134,5 +149,50 @@ describe('svg+text utils', function() { assertAnchorLink(node, 'https://abc.com/myFeature.jsp?name=abc&pwd=def'); }); }); + + it('allow basic spans', function() { + var node = mockTextSVGElement( + 'text' + ); + + expect(node.text()).toEqual('text'); + assertTspanStyle(node, null); + }); + + it('ignore unquoted styles in spans', function() { + var node = mockTextSVGElement( + 'text' + ); + + expect(node.text()).toEqual('text'); + assertTspanStyle(node, null); + }); + + it('allow quoted styles in spans', function() { + var node = mockTextSVGElement( + 'text' + ); + + expect(node.text()).toEqual('text'); + assertTspanStyle(node, 'quoted: yeah;'); + }); + + it('ignore extra stuff after span styles', function() { + var node = mockTextSVGElement( + 'text' + ); + + expect(node.text()).toEqual('text'); + assertTspanStyle(node, 'quoted: yeah;'); + }); + + it('escapes HTML entities in span styles', function() { + var node = mockTextSVGElement( + 'text' + ); + + expect(node.text()).toEqual('text'); + assertTspanStyle(node, 'quoted: yeah&\';;'); + }); }); });