Skip to content

webpack-dev-server vulnerable to DNS rebinding attack #887

Closed
@edmorley

Description

This issue was disclosed privately to @sokra via email on 2017-04-17, and a fix released in webpack-dev-server v2.4.3 / v1.16.4, which were released 2017-04-17.

I'm filing this issue retrospectively to clarify why the additional security check is necessary, in the hope that it makes people reconsider before turning it off (eg #882, #883).

--

Hi Tobias

Thank you for reaching out in webpack/webpack#4599.

The issue I've noticed is in webpack-dev-server - specifically:

  • it uses the node native http library - passing the hostname (default localhost) to http's listen() [1].
  • whilst the listen() docs [2] only refer to hostname, the actual behaviour is "resolve hostname to IP, bind to that IP and accept any connections to that IP regardless of the hostname used for the request" [3].
  • webpack-dev-server doesn't perform any verification of the Host header itself.
  • therefore it is vulnerable to DNS rebinding attacks, similar to that fixed in Rails [4] and Django [5].

Implications:

  • a malicious site can steal static content served by webpack-dev-server.
  • this can include any file from the directory where webpack-dev-server was run, not just those that would end up in dist/ (is this intentional, seems like an additional bug?).
  • worse, if webpack-dev-server's proxy feature [6] is being used, then this exposes any remote code execution vulnerabilities in the backend stack. This is particularly dangerous, since I would imagine it's common to use changeOrigin: True [7], which would bypass any Host header checks in the backend app - and so make typical Rails and Django setups RCE-vulnerable.

STR:

  1. Install nodejs 7.x, webpack 2.3.2 and webpack-dev-server 2.4.2.
  2. Create a new project based on https://webpack.js.org/guides/get-started/#using-webpack-with-a-config
  3. Run ./node_modules/.bin/webpack-dev-server
  4. In another terminal, try requests like:
  • curl -i zzz.malicious-site.com:8080/ --resolve zzz.malicious-site.com:8080:127.0.0.1
  • curl -i zzz.malicious-site.com:8080/package.json --resolve zzz.malicious-site.com:8080:127.0.0.1
  • curl -i zzz.malicious-site.com:8080/.git/config --resolve zzz.malicious-site.com:8080:127.0.0.1

Expected:
HTTP 400s or similar for each curl request, along with a "Invalid Host header" response body - similar to what Django's runserver does.

Actual:
HTTP 200s for cases where the file exists, along with the file contents.

Note: In the examples above, --resolve is being used to quickly emulate what a DNS rebinding attack could achieve, however the same is possible in a browser via a site using DNS rebinding - to demonstrate that this is true:

  1. Use the same project/setup as above
  2. Run DEBUG=express:router ./node_modules/.bin/webpack-dev-server --port 3000
  3. Visit http://dnsrebinder.net/
  4. Open the web console and watch the XHRs to http://<HASH>-bad.dnsrebinder.net:3000/not_found.
  5. When those XHRs change from HTTP 200s to HTTP 404s, it means the DNS for the subdomain has been updated to 127.0.0.1.
  6. Check the webpack-dev-server debug output - you'll see the GET /not_found showing the page circumvented CORS and hit the dev server.
  7. Inspect the headers/body of the HTTP 404 response in the browser web console - you'll see they are from the webpack-dev-server Express server - and could just have easily been an HTTP 200 disclosing local file content if the page had requested a more relevant URL.

dnsrebinder.net (source: [8]) is is a proof of concept for the Rails dev server RCE vulnerability mentioned above, hence the port 3000. It doesn't do anything interesting since it's relying on Rails specific behaviour, but it demonstrates the point without me needing to spin up something similar using AWS Route53 (I can if required). Note: A small number of DNS servers filter 127.0.0.1, if you can't repro, use Google DNS (8.8.8.8 / 8.8.4.4).

To fix this issue a Host header check needs to be added to webpack-dev-server, that's ideally enabled by default, with a big warning shown if there is a way to disable it.

In addition I think the nodejs http listen() docs [2] should be updated - but I'll file an issue there myself later.

[1] https://github.com/webpack/webpack-dev-server/blob/v2.4.2/bin/webpack-dev-server.js#L402
[2] https://nodejs.org/api/http.html#http_server_listen_port_hostname_backlog_callback
[3] https://stackoverflow.com/questions/21158686/node-js-express-limit-to-certain-hostname
[4] https://benmmurphy.github.io/blog/2016/07/11/rails-webconsole-dns-rebinding/
[5] https://docs.djangoproject.com/en/1.11/releases/1.10.3/#dns-rebinding-vulnerability-when-debug-true
[6] https://webpack.js.org/configuration/dev-server/#devserver-proxy
[7] https://github.com/chimurai/http-proxy-middleware#http-proxy-options
[8] https://github.com/benmmurphy/rebinder

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions