Skip to main content
  1. posts/

CVE-2018-15685 - Electron WebPreferences Remote Code Execution

I a remote code execution (RCE) vulnerability affecting apps with the ability to open nested child windows on Electron versions (3.0.0-beta.6, 2.0.7, 1.8.7, and 1.7.15). This vulnerability has been assigned the CVE identifier CVE-2018-15685.

POC Available at: https://github.com/matt-/CVE-2018-15685

What’s Electron? #

Electron is a framework that powers many of the applications you use every day. Slack, Atom, Visual Studio Code, WordPress Desktop, Github Desktop, Skype, and Google Chat are just a few applications built on the Electron framework. It allows a developer to quickly port a traditional web application to a native cross platform desktop application.

Vulnerability Details #

A recent post was made by @SecurityMB about a security issue in Google Chat where he could create a link that, when clicked, would redirect away from a Google site to content controlled by the attacker, but it would stay inside the electron app. Upon Google’s review, they found that the reported issue could lead to remote code execution and paid a sizeable bounty.

Although Google quickly fixed the issue related to the redirect, I could not help but wonder what exactly the path to code execution could be. The application is built on the Electron framework. Electron has a detailed security section (available here) and Google followed most of these practices. More specifically, they set “nodeIntegration” to false. Normally the webcontent of an Electron window has access to the Node Javascript engine underneath it. Node has many core libraries that give you access to the file system and let you execute code. This can be handy when building an app, but something you don’t want user controlled code to have access to. With “nodeIntegration” set to false, even with the ability to load remote content we “should” be safe.

However, after a bit of testing I came across a relatively simple payload that gave me access to the Node bindings:

open('about:blank').open('data:text/html,<script>document.write(process.cwd())</script>')

At first, I thought that this must have just been an oversight by the Google Chat team, but after digging some more, I discovered that was not the case. Every window that is created should have these properties set:

win.webPreferences = {
    allowRunningInsecureContent: false,
    contextIsolation: true,
    nodeIntegration: false,
    nativeWindowOpen: true
}

After a bit more digging and a couple of conversations with Luca Carettoni (who has done some really great research / work with Electron in the past), it became apparent that this was a bug in the core Electron framework. We came across what seemed to be related a to commit in an unmerged PR: https://github.com/electron/electron/pull/13222/commits/444b6f987bd793989999f64f40062f68217cf0ba

Properties of the window were not being inherited properly through nested windows and iframes. This meant vulnerable windows are able to be spawned by any application where:

  • user code runs inside an iframe, or
  • can create an iframe, or
  • if you open any of your windows with the “nativeWindowOpen: true” or “sandbox: true” options

(It’s a bit ironic that the two conditions leading to the issue are security controls)

Affected Versions #

  • Affected versions: Electron 3.0.0-beta.6, 2.0.7, 1.8.7, and 1.7.15
  • Patched versions: Electron 3.0.0-beta.7, 2.0.8, 1.8.8, and 1.7.16
  • CVE: CVE-2018-15685

You are impacted if:

  • You embed any remote user content, even in a sandbox
  • You accept user input with any XSS vulnerabilities
  • You use nativeWindowOpen: true or sandbox: true options

How It Works #

The vulnerability occurs when WebPreferences are not properly inherited by nested child windows. When a window is opened from within an iframe or using nativeWindowOpen: true, the child window should inherit the security settings from its parent. However, due to a bug in Electron, these preferences were not being inherited, causing the child window to fall back to insecure default settings.

The insecure defaults include:

  • nodeIntegration: true (allowing access to Node.js APIs)
  • contextIsolation: false (allowing prototype pollution attacks)

This means that even if a parent window has proper security settings, a nested child window could be created with full access to Node.js, allowing arbitrary code execution.

Proof of Concept #

The proof of concept demonstrates how a simple payload can bypass security controls:

open('about:blank').open('data:text/html,<script>document.write(process.cwd())</script>')

This payload:

  1. Opens a blank window
  2. From that window, opens another window with a data URI
  3. The nested window does not inherit the parent’s WebPreferences
  4. Falls back to insecure defaults, allowing access to process.cwd() and other Node.js APIs

A video demonstration is available showing the vulnerability in action.

The Impact #

This vulnerability is particularly dangerous because:

  • Full system compromise: An attacker can run arbitrary commands on the victim’s system, potentially leading to persistent access
  • Credential theft: The attacker could exfiltrate sensitive authentication tokens stored on the system
  • Data exfiltration: An attacker could steal sensitive files, code, or other private data stored on the victim’s system
  • Widespread impact: Many popular applications built on Electron are potentially affected

The attack is especially insidious because it can be triggered through normal application usage—just clicking a link or interacting with user-controlled content within an Electron app.

Mitigations #

We’ve published new versions of Electron which include fixes for this vulnerability: 3.0.0-beta.7, 2.0.8, 1.8.8, and 1.7.16. We urge all Electron developers to update their apps to the latest stable version immediately.

If for some reason you are unable to upgrade your Electron version, you can protect your app by blanket-calling event.preventDefault() on the new-window event for all webContents’. If you don’t use window.open or any child windows at all then this is also a valid mitigation for your app:

mainWindow.webContents.on('new-window', e => e.preventDefault())

If you rely on the ability of your child windows to make grandchild windows, then a third mitigation strategy is to use the following code on your top level window:

const enforceInheritance = (topWebContents) => {
  const handle = (webContents) => {
    webContents.on('new-window', (event, url, frameName, disposition, options) => {
      if (!options.webPreferences) {
        options.webPreferences = {}
      }
      Object.assign(options.webPreferences, topWebContents.getLastWebPreferences())
      if (options.webContents) {
        handle(options.webContents)
      }
    })
  }
  handle(topWebContents)
}

enforceInheritance(mainWindow.webContents)

This code will manually enforce that the top level window’s webPreferences is manually applied to all child windows infinitely deep.

If you don’t rely on child windows opening other child windows you can use a much simpler preventive technique:

mainWindow.webContents.on('new-window', (event, _, __, ___, options) => {
  if (options.webContents)
    options.webContents.on('new-window', e => e.preventDefault())
})

This will prevent all children opening deeper child windows, i.e. your windows will only have a depth of 1.

We strongly recommend you use the first mitigation (upgrading Electron) but if for whatever reason you can’t, the second one should also do the job just fine.

Recommendations for Electron Developers #

Follow the security guidelines at https://github.com/electron/electron/blob/master/docs/tutorial/security.md. These are not the default so you need to make sure these are in place.

Most importantly:

  • Set contextIsolation to true and nodeIntegration to false
  • Don’t allow user controlled HTML / Javascript (or an XSS) in an electron window if you can avoid it
  • Handle how new windows are created (or block them if they are not needed):
    mainWindow.webContents.on('new-window', e => e.preventDefault())
    

Protecting your Enterprise from similar CVEs #

As you leverage third party frameworks, be sure to validate the default security settings. Default values are often not those optimized for security.

Disclosure Timeline #

The Electron team responded quickly to the issue, and had a patch ready to go in just a couple of days. They were truly great to work with!

The Root Cause #

At the end of the day, the “root cause” of the issue is insecure default settings. In this case, with the default settings, a window has access to Node bindings and is not isolated. So, with this bug, when a window did not inherit properties as expected, it fell back to the insecure defaults. It would be great if the fallback was safe defaults. While changing an API can be a difficult process I hope this is something the Electron Team will work towards.

Takeaways #

This vulnerability highlights several important security lessons:

  1. Insecure defaults are dangerous: When frameworks fall back to insecure defaults, even well-intentioned developers can create vulnerable applications
  2. Inheritance bugs can be critical: When security properties aren’t properly inherited, it can completely bypass security controls
  3. Defense in depth: Multiple layers of security controls are essential, as any single layer can fail
  4. Follow security guidelines: Electron provides comprehensive security documentation that should be followed for all windows, not just some

We’d also like to thank the Electron team for being extremely responsive and for quickly working to provide a patch.

References #

  1. Electron Security Documentation
  2. CVE-2018-15685
  3. Electron Blog Post on the Vulnerability
  4. Proof of Concept Repository
  5. Electron GitHub PR #13222