SVG body is empty when using poltergeist as capybara driver
nathanl opened this issue · comments
(I'm using Poltergeist 1.13.0.)
My app allows users to add, and directly view, SVG images. Because SVGs can contain JavaScript, and users might unwittingly upload one that does, I want to make sure that the JavaScript is not executed when they directly view the SVG in their browser.
I can do that using a Content Security Policy header; any policy that doesn't explicitly allow script-src 'unsafe-inline'
will tell the browser "yeah and don't execute any <script>
tags you might find in this response".
Very good. Now I want to test that. I have a test (it's gherkin, but that's not important) like this:
# @javascript
Scenario: Viewing an SVG logo with potentially-malicious JS
Given there is an SVG logo called 'foo' that contains some JavaScript
When I view the 'foo' logo directly
Then the JS is not executed
It's that last step that I'm having trouble with.
If I have the @javascript
tag commented out, it uses a web driver that doesn't support JS, so of course the JS doesn't execute but that doesn't prove my CSP works. In the JS is not executed
step, I can inspect page.body
and see the SVG markup. For the sake of this discussion, suppose it's an Apple logo with malicious JS tacked on:
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="256px" height="315px" viewBox="0 0 256 315" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
<g>
<path d="M213.803394,167.030943 C214.2452,214.609646 255.542482,230.442639 256,230.644727 C255.650812,231.761357 249.401383,253.208293 234.24263,275.361446 C221.138555,294.513969 207.538253,313.596333 186.113759,313.991545 C165.062051,314.379442 158.292752,301.507828 134.22469,301.507828 C110.163898,301.507828 102.642899,313.596301 82.7151126,314.379442 C62.0350407,315.16201 46.2873831,293.668525 33.0744079,274.586162 C6.07529317,235.552544 -14.5576169,164.286328 13.147166,116.18047 C26.9103111,92.2909053 51.5060917,77.1630356 78.2026125,76.7751096 C98.5099145,76.3877456 117.677594,90.4371851 130.091705,90.4371851 C142.497945,90.4371851 165.790755,73.5415029 190.277627,76.0228474 C200.528668,76.4495055 229.303509,80.1636878 247.780625,107.209389 C246.291825,108.132333 213.44635,127.253405 213.803394,167.030988 M174.239142,50.1987033 C185.218331,36.9088319 192.607958,18.4081019 190.591988,0 C174.766312,0.636050225 155.629514,10.5457909 144.278109,23.8283506 C134.10507,35.5906758 125.195775,54.4170275 127.599657,72.4607932 C145.239231,73.8255433 163.259413,63.4970262 174.239142,50.1987249" fill="#000000"></path>
</g>
<script type="text/javascript">
console.log("This evil JS should be prevented by a Content Security Policy");
</script>
</svg>
If I uncomment the @javascript
tag, Capybara will use Poltergeist. In that case, when I check page.body
, I see an empty html body, like:
"<html><head></head><body></body></html>"
I can't test what the JavaScript does or doesn't do unless my web driver supports JavaScript. But when I use this web driver that supports JavaScript, the entire <script>
tag appears not to be there, and the body is a generic, empty HTML document.
Am I doing something wrong?
What are your "When I view the 'foo' logo directly" and "Then the JS is not executed" steps doing exactly? IIRC PhantomJS doesn't support just opening an SVG so that would explain why you're seeing an empty page, and you'd have to create a valid HTML page that includes the SVG.
"When I view the 'foo' logo directly" calls visit logo_path(id: logo_id)
, which hits a controller action that serves the image file, like this:
logo = Logo.find(params[:id])
send_data(logo.data, type: logo.mime_type, disposition: :inline)
The "JS is not executed" step isn't doing anything yet. My thought is that the SVG would contain some JavaScript which modifies one of its elements, and that this step would check for that modification and fail if it is there. Or perhaps there's a better way to check whether the JS has executed. But right now, it acts as if the SVG is entirely absent, so I can't check anything.
IIRC Poltergeist doesn't support just opening an SVG so that would explain why you're seeing an empty page, and you'd have to create a valid HTML page that includes the SVG.
Normally I use these images by having a <img>
whose src
points to the controller action which sends the image data. But the thing I'm trying to test is specifically that if someone views the image directly - eg if you visited this image - and the SVG had some JS in it, it wouldn't be executed.
If Poltergeist cannot test a response with mime type image/svg
, and capybara's non-JS driver can't execute the JS in an SVG, I don't see any way for me to test whether JS in an SVG is executed except by hand.
Hmmmm, I guess I'm wrong about PhantomJS not loading svgs directly
require 'capybara/poltergeist'
sess = Capybara::Session.new(:poltergeist)
sess.visit 'http://nathanmlong.com/images/rss_feed.svg'
puts sess.body
outputs the source of the svg. Check the actual data being returned from your controller.
Interesting. If I repeat your example, I do see the SVG data. However, when I run my test without the @javascript
tag (using :rack_test
), I can see my SVG, but when I run it with @javascript
(using Capybara.javascript_driver = :poltergeist
) I see only the empty body I showed above.
The server response appears to be OK from what I can see. The output to log/test.log
is identical in both cases. A puts
on the line before send_data
does appear when I'm using poltergeist, page.status_code
is 200, and page.response_headers
includes these:
"Content-Type"=>"image/svg",
"Content-Disposition"=>"inline",
"Content-Transfer-Encoding"=>"binary",
"Content-Length"=>"1575",
So it appears that Rails thinks it is sending SVG data, but page.body
doesn't reflect that when I use Poltergeist. Will tinker some more.
I found that visit logo_path(id: logo.id)
is returning {"status"=>"fail"}
, even though page.status_code
is 200
and page.response_headers
look right.
I went bundle open
diving in poltergeist
and websocket-driver
, with plenty of puts
debugging, but couldn't understand where or why the {"status"=>"fail"}
was generated. So far I'm stumped.
I still don't understand what's wrong, but I created a new, tiny Rails app, and in it, my js: true
test using poltereist does correctly see the contents of an SVG, does execute the SVG's inline javascript by default, and does ignore the SVG's inline javascript if a content security policy is correctly set.
So the issue is with my original app.
Aha! I found the issue. I had incorrectly set the mime type for SVG - I had used image/svg
when it should be image/svg+xml
. Apparently :rack_test
renders that anyway but :poltergeist
doesn't.
Glad you found it