Browser-based attacks on WebDriver implementations

Some context and analysis about attacks on in WebDriver implementations.

Summary of the vulnerabilities
References Type Affected Component Impact
CVE-2020-15660, Bug 1648964 CSRF geckodriver Remote Code Execution
CVE-2022-28108 CSRF Selenium server Remote Code Execution
Bug 1100097 Cross-origin/same-site request forgery chromedriver Remote Code Execution
CVE-2021-4138, Bug 1652612 DNS rebinding geckodriver Remote Code Execution
CVE-2022-28109 DNS rebinding Selenium server Remote Code Execution

Context: Zed Attack Proxy

I wanted to try Zed Attack Proxy (ZAP) for checking the vulnerability of web sites and services.

ZAP is (among other things) a meddler-in-the-middle (MITM) proxy server. It sits between your web browser and the web sites you want to test and captures all the HTTP and WebSocket traffic between the browser and web sites. You can display it is a nice table for further analysis. You can then replay a request, modify the request. It can as well intercept requests and responses and let you modify them on the fly. For this to work for HTTPS web sites, the browser must be configured to accept a local Certificate Authority (CA) generated by ZAP.

One way to proceed is to manually configure the browser to use ZAP as a proxy and manually add ZAP CA. Another approach it to let ZAP spawn a fresh browser instance which is automatically configured and somewhat controlled by ZAP:

Screenshot of Firefox launched by ZAP

In order to do this, ZAP relies on Selenium libraries which itself relies on the WebDriver protocol.

My initial goal was to understand how the communication between ZAP and the browser works and whether an attacker could possibly attack the user through this channel.

Introduction to WebDriver

WebDriver is a HTTP-based JSON-based W3C-standard protocol for browser automation. In all browsers, WebDriver protocol support is not implemented in the browser itself. Instead, the browser implements a native/proprietary automation/debugging protocol and a separate program translates between the native protocol and the standard WebDriver protocol.

WebDriver     WebDriver server      Browser
 client         (bridge)               |
  |                 |                  |
  v                 v                  v
[...]-------->[geckodriver]-------->[firefox]
         ^                     ^
         |                     |
      WebDriver            Marionette
      protocol              protocol

WebDriver     WebDriver server      Browser
 client         (bridge)               |
  |                 |                  |
  v                 v                  v
[...]-------->[chromedriver]-------->[Chrome]
         ^                     ^
         |                     |
      WebDriver            Chrome DevTools
      protocol              protocol

Here we are focusing on attacks based on the WebDriver protocol.

Note: WebDriver bidi

A new specification, WebDriver bidi, extending WebDriver for bidirectional communication is being worked on.

Firefox and WebDriver

For Firefox, geckodriver is implementing the WebDriver protocol. When run (geckodriver), it listens by default on 127.0.0.1:4444. We can then create a WebDriver session with:

curl -X POST http://localhost:4444/session \
  -H"Content-Type: application/json" \
  -d'{"capabilities":{}}'

Upon receiving this request geckodriver spawns a Firefox instance as:

firefox-esr -marionette -foreground -no-remote \
  -profile /tmp/rust_mozprofilenpAkba

The -marionette flag asks Firefox to listen to automation commands using its native protocol, Marionette1. By default, Firefox listens on localhost only.

The WebDriver server answers with a body like:

{"value":{
  "sessionId":"f5543763-25d3-40f9-b7e1-f9304357fb49",
  "capabilities":{
    "acceptInsecureCerts":false,
    "browserName":"firefox",
    "browserVersion":"68.10.0",
    "moz:accessibilityChecks":false,
    "moz:buildID":"20200622191537",
    "moz:geckodriverVersion":"0.26.0",
    "moz:headless":false,
    "moz:processID":3099,
    "moz:profile":"/tmp/rust_mozprofilenpAkba",
    "moz:shutdownTimeout":60000,
    "moz:useNonSpecCompliantPointerOrigin":false,
    "moz:webdriverClick":true,
    "pageLoadStrategy":"normal",
    "platformName":"linux",
    "platformVersion":"4.19.0-9-amd64",
    "rotatable":false,
    "setWindowRect":true,
    "strictFileInteractability":false,
    "timeouts":{
      "implicit":0,
      "pageLoad":300000,
      "script":30000
    },
    "unhandledPromptBehavior":"dismiss and notify"
  }
}}

The sessionId parameter identifies the session and will be used to control the WebDriver session.

We can now for example ask the browser to navigate to a given URI with:

curl -X POST  "http://localhost:4444/session/f5543763-25d3-40f9-b7e1-f9304357fb49/url" \
  -H"Content-Type: application/json" \
  -d '{"url": "https://www.gabriel.urdhr.fr/"}'

Geckodriver only allows a single WebDriver session at given time. While an existing session is still present, it will refuse the creation of a new WebDriver session.

Chromium and WebDriver

chromedriver is the WebDriver implementation for Chromium. By default, it listens on localhost only (127.0.0.1::9515 and [::1]:9515).

When a session is created, it spawns a Chrome instance as:

/opt/google/chrome/chrome \
  --disable-background-networking --disable-client-side-phishing-detection \
  --disable-default-apps --disable-hang-monitor --disable-popup-blocking \
  --disable-prompt-on-repost --disable-sync --enable-automation \
  --enable-blink-features=ShadowDOMV0 --enable-logging \
  --load-extension=/tmp/.org.chromium.Chromium.1IEzDp/internal --log-level=0 \
  --no-first-run --password-store=basic --remote-debugging-port=0 \
  --test-type=webdriver --use-mock-keychain \
  --user-data-dir=/tmp/.org.chromium.Chromium.NU3KFK data:,

The -remote-debugging-port=0 argument makes Chrome listen using its native automation/debugging protocol, Chrome DevTools Protocol (CDP). When the value 0 is used, Chrome chooses the port by itself. Chrome listens on 127.0.0.1 only.

Security of the WebDriver protocol

Design

The WebDriver protocol is that it is based on HTTP. Moreover, none of the existing implementations support any form of authentication2. The service is only accessible from the local machine (bound to localhost) by default in all implementations. The security of the WebDriver service is solely based on the fact that the service is not (directly) accessible from a remote attacker.

Here is an extract of the “security” section of the WebDriver specification:

To prevent arbitrary machines on the network from connecting and creating sessions, it is suggested that only connections from loopback devices are allowed by default.

The remote end can include a configuration option to limit the accepted IP range allowed to connect and make requests. The default setting for this might be to limit connections to the IPv4 localhost CIDR range 127.0.0.0/8 and the IPv6 localhost address ::1. [RFC4632]

Browser-mediated attacks

Two classes of vulnerabilities are very often found in local-services which are only secured by the fact that they are not accessible by remote attackers3:

  • Cross Site Request forgery (CSRF) attacks;
  • DNS rebinding attacks.

The idea of these two types of attacks is the same: a malicious web site tricks the user browser into issuing requests to the vulnerable service. The service is not directly accessible by the remote attacker but is potentially indirectly accessible through the user browser.

Three different WebDriver implementations were vulnerabilities to browser-mediated attacks:

  • geckodriver;
  • chromedriver;
  • Selenium standalone server / Selenium Grid.

In all cases, this could be used by a malicious website for arbitrary code execution.

In the case of chromedriver however, CSRF attacks were previously possible. Currently, the attack is only possible in cross-origin/same-site so the impact is very limited. As we have seen local users can already attack the service directly. A remote attacker would have to exploit another vulnerability in another localhost-bound origin in order be able to exploit the vulnerability.

In the case of Geckodriver, the exposure is limited by the fact that only a single session can be used at the same time per geckodriver instance. For example, when launched by ZAP, ZAP directly creates a session so the geckodriver instance is vulnerable for a very short duration.

I would argue that these vulnerabilities are possible because of the weak security model of WebDriver instances which do not support any form of authentication. However both CSRF and DNS-rebinding attacks can be prevented without adding dedicated support for authentication in the implementations.

Attacks from local users

Another security concern, is that any local user can access these services. As we have seen, we can execute arbitrary commands over WebDriver for both geckodriver and chromedriver. This means that running geckodriver or chromedriver gives a way for any other local user to execute arbitrary commands on our behalf.

Some solutions to solve both browser-based attacks and local-user attacks would be:

  • adding optional support for some form of HTTP-level authentication;
  • adding support for binding the service on a PF_UNIX socket4.

Conclusion

The WebDriver specification should probably:

  • mention the risk of CSRF and DNS rebinding attack (in the security section);
  • mention the risk of attacks by local users (in the security section);
  • suggest support for HTTP-based authentication;
  • explicitly allow to validate Content-Type of WebDriver requests (as a simple way to prevent CSRF attacks).

More generally, many debugging interfaces allow remote code execution (often by design) but often lack any form of authentication. Some are web-based and may be vulnerable to CSRF and/or DNS-rebinding attacks. Protection against local users is usually not considered an issue. For example:

  • gdbserver may listen on a TCP port but does not support any form of authentication;
  • Werkzeug debug PIN protection does not protect against local users by default (because of the way the PIN is generated);
  • Java debugging is using TCP (-Xdebug -Xrunjdwp:transport=dt_socket,address=8888,server=y) without any authentication.

  1. This is a JSON-based Remote Procedure call (RPC) protocol. In contrast to the WebDriver protocol, it is not based on HTTP. Message framing is done by prefixing each message by its length (eg. 81:[0,1,"WebDriver:NewSession",{"acceptInsecureCerts":true,"browserName":"firefox"}]). 

  2. Authentication is not mentionned in the WebDriver specification. While it would be theoretically possible to support HTTP-based authentication, no implementataion (client or server) supports this as far as I know. 

  3. This includes a lot of services exposed on the LAN by IoT devices, printers, smart TVs, etc. and localhost-bound services of many applications. As we have already seen, UPnP services are an intersesting example of this kind of services and many UPnP services are vulnerable to CSRF and DNS rebinding attacks. 

  4. We cannot reach PF_UNIX sockets through CSRF or DNS rebinding. In addition, we can prevent other local users from connecting to such a socket.