diff --git a/src/lib/svg_text_utils.js b/src/lib/svg_text_utils.js
index 922c4b0213e..fe995cc74f7 100644
--- a/src/lib/svg_text_utils.js
+++ b/src/lib/svg_text_utils.js
@@ -242,6 +242,15 @@ util.plainText = function(_str) {
return (_str || '').replace(STRIP_TAGS, ' ');
};
+function encodeForHTML(_str) {
+ return (_str || '').replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''')
+ .replace(/\//g, '/');
+}
+
function convertToSVG(_str) {
var htmlEntitiesDecoded = Plotly.util.html_entity_decode(_str);
var result = htmlEntitiesDecoded
@@ -270,15 +279,14 @@ function convertToSVG(_str) {
// remove quotes, leading '=', replace '&' with '&'
var href = extra.substr(4)
.replace(/["']/g, '')
- .replace(/=/, '')
- .replace(/&/g, '&');
+ .replace(/=/, '');
// check protocol
var dummyAnchor = document.createElement('a');
dummyAnchor.href = href;
if(PROTOCOLS.indexOf(dummyAnchor.protocol) === -1) return '';
- return '';
+ return '';
}
}
else if(tag === 'br') return '
';
@@ -302,7 +310,7 @@ function convertToSVG(_str) {
// most of the svg css users will care about is just like html,
// but font color is different. Let our users ignore this.
extraStyle = extraStyle[1].replace(/(^|;)\s*color:/, '$1 fill:');
- style = (style ? style + ';' : '') + extraStyle;
+ style = (style ? style + ';' : '') + encodeForHTML(extraStyle);
}
return tspanStart + (style ? ' style="' + style + '"' : '') + '>';
diff --git a/test/jasmine/tests/svg_text_utils_test.js b/test/jasmine/tests/svg_text_utils_test.js
index 6d11560a105..be4601743c8 100644
--- a/test/jasmine/tests/svg_text_utils_test.js
+++ b/test/jasmine/tests/svg_text_utils_test.js
@@ -25,6 +25,11 @@ describe('svg+text utils', function() {
expect(a.attr('xlink:show')).toBe(href === null ? null : 'new');
}
+ function assertTspanStyle(node, style) {
+ var tspan = node.select('tspan');
+ expect(tspan.attr('style')).toBe(style);
+ }
+
function assertAnchorAttrs(node) {
var a = node.select('a');
@@ -75,6 +80,16 @@ describe('svg+text utils', function() {
assertAnchorLink(node, null);
});
+ it('whitelist relative hrefs (interpreted as http)', function() {
+ var node = mockTextSVGElement(
+ 'mylink'
+ );
+
+ expect(node.text()).toEqual('mylink');
+ assertAnchorAttrs(node);
+ assertAnchorLink(node, '/mylink');
+ });
+
it('whitelist http hrefs', function() {
var node = mockTextSVGElement(
'bl.ocks.org'
@@ -134,5 +149,50 @@ describe('svg+text utils', function() {
assertAnchorLink(node, 'https://abc.com/myFeature.jsp?name=abc&pwd=def');
});
});
+
+ it('allow basic spans', function() {
+ var node = mockTextSVGElement(
+ 'text'
+ );
+
+ expect(node.text()).toEqual('text');
+ assertTspanStyle(node, null);
+ });
+
+ it('ignore unquoted styles in spans', function() {
+ var node = mockTextSVGElement(
+ 'text'
+ );
+
+ expect(node.text()).toEqual('text');
+ assertTspanStyle(node, null);
+ });
+
+ it('allow quoted styles in spans', function() {
+ var node = mockTextSVGElement(
+ 'text'
+ );
+
+ expect(node.text()).toEqual('text');
+ assertTspanStyle(node, 'quoted: yeah;');
+ });
+
+ it('ignore extra stuff after span styles', function() {
+ var node = mockTextSVGElement(
+ 'text'
+ );
+
+ expect(node.text()).toEqual('text');
+ assertTspanStyle(node, 'quoted: yeah;');
+ });
+
+ it('escapes HTML entities in span styles', function() {
+ var node = mockTextSVGElement(
+ 'text'
+ );
+
+ expect(node.text()).toEqual('text');
+ assertTspanStyle(node, 'quoted: yeah&\';;');
+ });
});
});