From 7ee7107f8d357930e0909bda8abf21f03adb7fd9 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Wed, 27 Jul 2016 12:28:01 -0400 Subject: [PATCH] Cherry-pick #736 --- src/lib/svg_text_utils.js | 17 ++- test/jasmine/tests/svg_text_utils_test.js | 138 ++++++++++++++++++++++ 2 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 test/jasmine/tests/svg_text_utils_test.js diff --git a/src/lib/svg_text_utils.js b/src/lib/svg_text_utils.js index f1b9368dc72..dfb3dced0d5 100644 --- a/src/lib/svg_text_utils.js +++ b/src/lib/svg_text_utils.js @@ -221,6 +221,8 @@ var TAG_STYLES = { em: 'font-style:italic;font-weight:bold' }; +var PROTOCOLS = ['http:', 'https:', 'mailto:']; + var STRIP_TAGS = new RegExp(']*)?/?>', 'g'); util.plainText = function(_str){ @@ -252,7 +254,20 @@ function convertToSVG(_str){ if(tag === 'a'){ if(close) return ''; else if(extra.substr(0,4).toLowerCase() !== 'href') return ''; - else return ''; + else { + // 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 ''; + } } else if(tag === 'br') return '
'; else if(close) { diff --git a/test/jasmine/tests/svg_text_utils_test.js b/test/jasmine/tests/svg_text_utils_test.js new file mode 100644 index 00000000000..6d11560a105 --- /dev/null +++ b/test/jasmine/tests/svg_text_utils_test.js @@ -0,0 +1,138 @@ +var d3 = require('d3'); + +var util = require('@src/lib/svg_text_utils'); + + +describe('svg+text utils', function() { + 'use strict'; + + describe('convertToTspans should', function() { + + function mockTextSVGElement(txt) { + return d3.select('body') + .append('svg') + .attr('id', 'text') + .append('text') + .text(txt) + .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('check for XSS attack in href', function() { + var node = mockTextSVGElement( + '
XSS' + ); + + expect(node.text()).toEqual('XSS'); + assertAnchorAttrs(node); + assertAnchorLink(node, null); + }); + + it('check for XSS attack in href (with plenty of white spaces)', function() { + var node = mockTextSVGElement( + 'XSS' + ); + + expect(node.text()).toEqual('XSS'); + assertAnchorAttrs(node); + assertAnchorLink(node, null); + }); + + it('whitelist http hrefs', function() { + var node = mockTextSVGElement( + 'bl.ocks.org' + ); + + expect(node.text()).toEqual('bl.ocks.org'); + assertAnchorAttrs(node); + assertAnchorLink(node, 'http://bl.ocks.org/'); + }); + + it('whitelist https hrefs', function() { + var node = mockTextSVGElement( + 'plot.ly' + ); + + expect(node.text()).toEqual('plot.ly'); + assertAnchorAttrs(node); + assertAnchorLink(node, 'https://plot.ly'); + }); + + it('whitelist mailto hrefs', function() { + var node = mockTextSVGElement( + 'support' + ); + + expect(node.text()).toEqual('support'); + 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'); + }); + }); + }); +});