Skip to content

Commit 63bcc3c

Browse files
committed
Cherry-pick #736 into a v1.1.1 maintenance release
1 parent 7314a2c commit 63bcc3c

File tree

3 files changed

+159
-6
lines changed

3 files changed

+159
-6
lines changed

circle.yml

+5-5
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,17 @@ general:
55

66
machine:
77
node:
8-
version: 4.2.1
8+
version: 4.2.1
99
services:
1010
- docker
11-
11+
1212

1313
dependencies:
14-
pre:
15-
- docker pull plotly/imageserver:latest
14+
pre:
15+
- docker pull plotly/testbed:1.1.0
1616
post:
1717
- npm run cibuild
18-
- docker run -d --name myimageserver -v $PWD:/var/www/streambed/image_server/plotly.js -p 9010:9010 plotly/imageserver:latest; sleep 20
18+
- docker run -d --name myimageserver -v $PWD:/var/www/streambed/image_server/plotly.js -p 9010:9010 plotly/testbed:1.1.0; sleep 20
1919

2020
test:
2121
override:

src/lib/svg_text_utils.js

+16-1
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,8 @@ var TAG_STYLES = {
221221
em: 'font-style:italic;font-weight:bold'
222222
};
223223

224+
var PROTOCOLS = ['http:', 'https:', 'mailto:'];
225+
224226
var STRIP_TAGS = new RegExp('</?(' + Object.keys(TAG_STYLES).join('|') + ')( [^>]*)?/?>', 'g');
225227

226228
util.plainText = function(_str){
@@ -252,7 +254,20 @@ function convertToSVG(_str){
252254
if(tag === 'a'){
253255
if(close) return '</a>';
254256
else if(extra.substr(0,4).toLowerCase() !== 'href') return '<a>';
255-
else return '<a xlink:show="new" xlink:href' + extra.substr(4) + '>';
257+
else {
258+
// remove quotes, leading '=', replace '&' with '&amp;'
259+
var href = extra.substr(4)
260+
.replace(/["']/g, '')
261+
.replace(/=/, '')
262+
.replace(/&/g, '&amp;');
263+
264+
// check protocol
265+
var dummyAnchor = document.createElement('a');
266+
dummyAnchor.href = href;
267+
if(PROTOCOLS.indexOf(dummyAnchor.protocol) === -1) return '<a>';
268+
269+
return '<a xlink:show="new" xlink:href="' + href + '">';
270+
}
256271
}
257272
else if(tag === 'br') return '<br>';
258273
else if(close) {
+138
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
var d3 = require('d3');
2+
3+
var util = require('@src/lib/svg_text_utils');
4+
5+
6+
describe('svg+text utils', function() {
7+
'use strict';
8+
9+
describe('convertToTspans should', function() {
10+
11+
function mockTextSVGElement(txt) {
12+
return d3.select('body')
13+
.append('svg')
14+
.attr('id', 'text')
15+
.append('text')
16+
.text(txt)
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;
52+
}
53+
54+
afterEach(function() {
55+
d3.select('#text').remove();
56+
});
57+
58+
it('check for XSS attack in href', function() {
59+
var node = mockTextSVGElement(
60+
'<a href="javascript:alert(\'attack\')">XSS</a>'
61+
);
62+
63+
expect(node.text()).toEqual('XSS');
64+
assertAnchorAttrs(node);
65+
assertAnchorLink(node, null);
66+
});
67+
68+
it('check for XSS attack in href (with plenty of white spaces)', function() {
69+
var node = mockTextSVGElement(
70+
'<a href = " javascript:alert(\'attack\')">XSS</a>'
71+
);
72+
73+
expect(node.text()).toEqual('XSS');
74+
assertAnchorAttrs(node);
75+
assertAnchorLink(node, null);
76+
});
77+
78+
it('whitelist http hrefs', function() {
79+
var node = mockTextSVGElement(
80+
'<a href="http://bl.ocks.org/">bl.ocks.org</a>'
81+
);
82+
83+
expect(node.text()).toEqual('bl.ocks.org');
84+
assertAnchorAttrs(node);
85+
assertAnchorLink(node, 'http://bl.ocks.org/');
86+
});
87+
88+
it('whitelist https hrefs', function() {
89+
var node = mockTextSVGElement(
90+
'<a href="https://plot.ly">plot.ly</a>'
91+
);
92+
93+
expect(node.text()).toEqual('plot.ly');
94+
assertAnchorAttrs(node);
95+
assertAnchorLink(node, 'https://plot.ly');
96+
});
97+
98+
it('whitelist mailto hrefs', function() {
99+
var node = mockTextSVGElement(
100+
'<a href="mailto:[email protected]">support</a>'
101+
);
102+
103+
expect(node.text()).toEqual('support');
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+
});
136+
});
137+
});
138+
});

0 commit comments

Comments
 (0)