teampoltergeist / poltergeist

A PhantomJS driver for Capybara
MIT License
2.5k stars 415 forks source link

SVG body is empty when using poltergeist as capybara driver #865

Closed nathanl closed 7 years ago

nathanl commented 7 years ago

(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?

twalpole commented 7 years ago

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.

nathanl commented 7 years ago

"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.

twalpole commented 7 years ago

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.

nathanl commented 7 years ago

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.

nathanl commented 7 years ago

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.

nathanl commented 7 years ago

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.

nathanl commented 7 years ago

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.

twalpole commented 7 years ago

Glad you found it