Skip to content

Commit 12bed39

Browse files
authored
Merge pull request #736 from plotly/convert-to-svg
A few fixes in convertToTspans
2 parents d96e5e1 + f6a5c67 commit 12bed39

File tree

2 files changed

+94
-15
lines changed

2 files changed

+94
-15
lines changed

src/lib/svg_text_utils.js

+13-3
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,11 @@ util.convertToTspans = function(_context, _callback) {
9595
visibility: 'visible',
9696
'white-space': 'pre'
9797
});
98+
9899
result = _context.appendSVG(converted);
100+
99101
if(!result) _context.text(str);
102+
100103
if(_context.select('a').size()) {
101104
// at least in Chrome, pointer-events does not seem
102105
// to be honored in children of <text> elements
@@ -246,6 +249,7 @@ function convertToSVG(_str) {
246249
var match = d.match(/<(\/?)([^ >]*)\s*(.*)>/i),
247250
tag = match && match[2].toLowerCase(),
248251
style = TAG_STYLES[tag];
252+
249253
if(style !== undefined) {
250254
var close = match[1],
251255
extra = match[3],
@@ -263,12 +267,18 @@ function convertToSVG(_str) {
263267
if(close) return '</a>';
264268
else if(extra.substr(0, 4).toLowerCase() !== 'href') return '<a>';
265269
else {
266-
var dummyAnchor = document.createElement('a');
267-
dummyAnchor.href = extra.substr(4).replace(/["'=]/g, '');
270+
// remove quotes, leading '=', replace '&' with '&amp;'
271+
var href = extra.substr(4)
272+
.replace(/["']/g, '')
273+
.replace(/=/, '')
274+
.replace(/&/g, '&amp;');
268275

276+
// check protocol
277+
var dummyAnchor = document.createElement('a');
278+
dummyAnchor.href = href;
269279
if(PROTOCOLS.indexOf(dummyAnchor.protocol) === -1) return '<a>';
270280

271-
return '<a xlink:show="new" xlink:href' + extra.substr(4) + '>';
281+
return '<a xlink:show="new" xlink:href="' + href + '">';
272282
}
273283
}
274284
else if(tag === 'br') return '<br>';

test/jasmine/tests/svg_text_utils_test.js

+81-12
Original file line numberDiff line numberDiff line change
@@ -6,64 +6,133 @@ var util = require('@src/lib/svg_text_utils');
66
describe('svg+text utils', function() {
77
'use strict';
88

9-
describe('convertToTspans', function() {
9+
describe('convertToTspans should', function() {
1010

1111
function mockTextSVGElement(txt) {
1212
return d3.select('body')
1313
.append('svg')
1414
.attr('id', 'text')
1515
.append('text')
1616
.text(txt)
17-
.call(util.convertToTspans);
17+
.call(util.convertToTspans)
18+
.attr('transform', 'translate(50,50)');
19+
}
20+
21+
function assertAnchorLink(node, href) {
22+
var a = node.select('a');
23+
24+
expect(a.attr('xlink:href')).toBe(href);
25+
expect(a.attr('xlink:show')).toBe(href === null ? null : 'new');
26+
}
27+
28+
function assertAnchorAttrs(node) {
29+
var a = node.select('a');
30+
31+
var WHITE_LIST = ['xlink:href', 'xlink:show', 'style'],
32+
attrs = listAttributes(a.node());
33+
34+
// check that no other attribute are found in anchor,
35+
// which can be lead to XSS attacks.
36+
37+
var hasWrongAttr = attrs.some(function(attr) {
38+
return WHITE_LIST.indexOf(attr) === -1;
39+
});
40+
41+
expect(hasWrongAttr).toBe(false);
42+
}
43+
44+
function listAttributes(node) {
45+
var items = Array.prototype.slice.call(node.attributes);
46+
47+
var attrs = items.map(function(item) {
48+
return item.name;
49+
});
50+
51+
return attrs;
1852
}
1953

2054
afterEach(function() {
2155
d3.select('#text').remove();
2256
});
2357

24-
it('checks for XSS attack in href', function() {
58+
it('check for XSS attack in href', function() {
2559
var node = mockTextSVGElement(
2660
'<a href="javascript:alert(\'attack\')">XSS</a>'
2761
);
2862

2963
expect(node.text()).toEqual('XSS');
30-
expect(node.select('a').attr('xlink:href')).toBe(null);
64+
assertAnchorAttrs(node);
65+
assertAnchorLink(node, null);
3166
});
3267

33-
it('checks for XSS attack in href (with plenty of white spaces)', function() {
68+
it('check for XSS attack in href (with plenty of white spaces)', function() {
3469
var node = mockTextSVGElement(
3570
'<a href = " javascript:alert(\'attack\')">XSS</a>'
3671
);
3772

3873
expect(node.text()).toEqual('XSS');
39-
expect(node.select('a').attr('xlink:href')).toBe(null);
74+
assertAnchorAttrs(node);
75+
assertAnchorLink(node, null);
4076
});
4177

42-
it('whitelists http hrefs', function() {
78+
it('whitelist http hrefs', function() {
4379
var node = mockTextSVGElement(
4480
'<a href="http://bl.ocks.org/">bl.ocks.org</a>'
4581
);
4682

4783
expect(node.text()).toEqual('bl.ocks.org');
48-
expect(node.select('a').attr('xlink:href')).toEqual('http://bl.ocks.org/');
84+
assertAnchorAttrs(node);
85+
assertAnchorLink(node, 'http://bl.ocks.org/');
4986
});
5087

51-
it('whitelists https hrefs', function() {
88+
it('whitelist https hrefs', function() {
5289
var node = mockTextSVGElement(
5390
'<a href="https://plot.ly">plot.ly</a>'
5491
);
5592

5693
expect(node.text()).toEqual('plot.ly');
57-
expect(node.select('a').attr('xlink:href')).toEqual('https://plot.ly');
94+
assertAnchorAttrs(node);
95+
assertAnchorLink(node, 'https://plot.ly');
5896
});
5997

60-
it('whitelists mailto hrefs', function() {
98+
it('whitelist mailto hrefs', function() {
6199
var node = mockTextSVGElement(
62100
'<a href="mailto:[email protected]">support</a>'
63101
);
64102

65103
expect(node.text()).toEqual('support');
66-
expect(node.select('a').attr('xlink:href')).toEqual('mailto:[email protected]');
104+
assertAnchorAttrs(node);
105+
assertAnchorLink(node, 'mailto:[email protected]');
106+
});
107+
108+
it('wrap XSS attacks in href', function() {
109+
var textCases = [
110+
'<a href="XSS\" onmouseover=&quot;alert(1)\" style=&quot;font-size:300px">Subtitle</a>',
111+
'<a href="XSS&quot; onmouseover=&quot;alert(1)&quot; style=&quot;font-size:300px">Subtitle</a>'
112+
];
113+
114+
textCases.forEach(function(textCase) {
115+
var node = mockTextSVGElement(textCase);
116+
117+
expect(node.text()).toEqual('Subtitle');
118+
assertAnchorAttrs(node);
119+
assertAnchorLink(node, 'XSS onmouseover=alert(1) style=font-size:300px');
120+
});
121+
});
122+
123+
it('should keep query parameters in href', function() {
124+
var textCases = [
125+
'<a href="https://abc.com/myFeature.jsp?name=abc&pwd=def">abc.com?shared-key</a>',
126+
'<a href="https://abc.com/myFeature.jsp?name=abc&amp;pwd=def">abc.com?shared-key</a>'
127+
];
128+
129+
textCases.forEach(function(textCase) {
130+
var node = mockTextSVGElement(textCase);
131+
132+
assertAnchorAttrs(node);
133+
expect(node.text()).toEqual('abc.com?shared-key');
134+
assertAnchorLink(node, 'https://abc.com/myFeature.jsp?name=abc&pwd=def');
135+
});
67136
});
68137
});
69138
});

0 commit comments

Comments
 (0)