diff --git a/src/lib/svg_text_utils.js b/src/lib/svg_text_utils.js index 33e9d59bb9d..922c4b0213e 100644 --- a/src/lib/svg_text_utils.js +++ b/src/lib/svg_text_utils.js @@ -95,8 +95,11 @@ util.convertToTspans = function(_context, _callback) { visibility: 'visible', 'white-space': 'pre' }); + result = _context.appendSVG(converted); + if(!result) _context.text(str); + if(_context.select('a').size()) { // at least in Chrome, pointer-events does not seem // to be honored in children of elements @@ -246,6 +249,7 @@ function convertToSVG(_str) { var match = d.match(/<(\/?)([^ >]*)\s*(.*)>/i), tag = match && match[2].toLowerCase(), style = TAG_STYLES[tag]; + if(style !== undefined) { var close = match[1], extra = match[3], @@ -263,12 +267,18 @@ function convertToSVG(_str) { if(close) return ''; else if(extra.substr(0, 4).toLowerCase() !== 'href') return ''; else { - var dummyAnchor = document.createElement('a'); - dummyAnchor.href = extra.substr(4).replace(/["'=]/g, ''); + // remove quotes, leading '=', replace '&' with '&' + var href = extra.substr(4) + .replace(/["']/g, '') + .replace(/=/, '') + .replace(/&/g, '&'); + // check protocol + var dummyAnchor = document.createElement('a'); + dummyAnchor.href = href; if(PROTOCOLS.indexOf(dummyAnchor.protocol) === -1) return ''; - return ''; + return ''; } } else if(tag === 'br') return '
'; diff --git a/test/jasmine/tests/svg_text_utils_test.js b/test/jasmine/tests/svg_text_utils_test.js index ef7e4e190cb..6d11560a105 100644 --- a/test/jasmine/tests/svg_text_utils_test.js +++ b/test/jasmine/tests/svg_text_utils_test.js @@ -6,7 +6,7 @@ var util = require('@src/lib/svg_text_utils'); describe('svg+text utils', function() { 'use strict'; - describe('convertToTspans', function() { + describe('convertToTspans should', function() { function mockTextSVGElement(txt) { return d3.select('body') @@ -14,56 +14,125 @@ describe('svg+text utils', function() { .attr('id', 'text') .append('text') .text(txt) - .call(util.convertToTspans); + .call(util.convertToTspans) + .attr('transform', 'translate(50,50)'); + } + + function assertAnchorLink(node, href) { + var a = node.select('a'); + + expect(a.attr('xlink:href')).toBe(href); + expect(a.attr('xlink:show')).toBe(href === null ? null : 'new'); + } + + function assertAnchorAttrs(node) { + var a = node.select('a'); + + var WHITE_LIST = ['xlink:href', 'xlink:show', 'style'], + attrs = listAttributes(a.node()); + + // check that no other attribute are found in anchor, + // which can be lead to XSS attacks. + + var hasWrongAttr = attrs.some(function(attr) { + return WHITE_LIST.indexOf(attr) === -1; + }); + + expect(hasWrongAttr).toBe(false); + } + + function listAttributes(node) { + var items = Array.prototype.slice.call(node.attributes); + + var attrs = items.map(function(item) { + return item.name; + }); + + return attrs; } afterEach(function() { d3.select('#text').remove(); }); - it('checks for XSS attack in href', function() { + it('check for XSS attack in href', function() { var node = mockTextSVGElement( '
XSS' ); expect(node.text()).toEqual('XSS'); - expect(node.select('a').attr('xlink:href')).toBe(null); + assertAnchorAttrs(node); + assertAnchorLink(node, null); }); - it('checks for XSS attack in href (with plenty of white spaces)', function() { + it('check for XSS attack in href (with plenty of white spaces)', function() { var node = mockTextSVGElement( 'XSS' ); expect(node.text()).toEqual('XSS'); - expect(node.select('a').attr('xlink:href')).toBe(null); + assertAnchorAttrs(node); + assertAnchorLink(node, null); }); - it('whitelists http hrefs', function() { + it('whitelist http hrefs', function() { var node = mockTextSVGElement( 'bl.ocks.org' ); expect(node.text()).toEqual('bl.ocks.org'); - expect(node.select('a').attr('xlink:href')).toEqual('http://bl.ocks.org/'); + assertAnchorAttrs(node); + assertAnchorLink(node, 'http://bl.ocks.org/'); }); - it('whitelists https hrefs', function() { + it('whitelist https hrefs', function() { var node = mockTextSVGElement( 'plot.ly' ); expect(node.text()).toEqual('plot.ly'); - expect(node.select('a').attr('xlink:href')).toEqual('https://plot.ly'); + assertAnchorAttrs(node); + assertAnchorLink(node, 'https://plot.ly'); }); - it('whitelists mailto hrefs', function() { + it('whitelist mailto hrefs', function() { var node = mockTextSVGElement( 'support' ); expect(node.text()).toEqual('support'); - expect(node.select('a').attr('xlink:href')).toEqual('mailto:support@plot.ly'); + assertAnchorAttrs(node); + assertAnchorLink(node, 'mailto:support@plot.ly'); + }); + + it('wrap XSS attacks in href', function() { + var textCases = [ + 'Subtitle', + 'Subtitle' + ]; + + textCases.forEach(function(textCase) { + var node = mockTextSVGElement(textCase); + + expect(node.text()).toEqual('Subtitle'); + assertAnchorAttrs(node); + assertAnchorLink(node, 'XSS onmouseover=alert(1) style=font-size:300px'); + }); + }); + + it('should keep query parameters in href', function() { + var textCases = [ + 'abc.com?shared-key', + 'abc.com?shared-key' + ]; + + textCases.forEach(function(textCase) { + var node = mockTextSVGElement(textCase); + + assertAnchorAttrs(node); + expect(node.text()).toEqual('abc.com?shared-key'); + assertAnchorLink(node, 'https://abc.com/myFeature.jsp?name=abc&pwd=def'); + }); }); }); });