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('?(' + Object.keys(TAG_STYLES).join('|') + ')( [^>]*)?/?>', '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');
+ });
+ });
+ });
+});