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:
- Install nodejs 7.x, webpack 2.3.2 and webpack-dev-server 2.4.2.
- Create a new project based on https://webpack.js.org/guides/get-started/#using-webpack-with-a-config
- Run
./node_modules/.bin/webpack-dev-server
- 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:
- Use the same project/setup as above
- Run
DEBUG=express:router ./node_modules/.bin/webpack-dev-server --port 3000
- Visit http://dnsrebinder.net/
- Open the web console and watch the XHRs to
http://<HASH>-bad.dnsrebinder.net:3000/not_found
. - 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
. - Check the webpack-dev-server debug output - you'll see the
GET /not_found
showing the page circumvented CORS and hit the dev server. - 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