Skip to content
Permalink
Browse files

security: allow to block desktopCapturer.getSources() calls

  • Loading branch information...
miniak committed Dec 5, 2018
1 parent 764a10f commit fe1397e0399c854e460fac5b2c9b17905d9f6b5b
@@ -398,6 +398,16 @@ non-minimized.
This event is guaranteed to be emitted after the `ready` event of `app`
gets emitted.

### Event: 'desktop-capturer-get-sources'

Returns:

* `event` Event
* `webContents` [WebContents](web-contents.md)

Emitted when `desktopCapturer.getSources()` is called in the renderer process of `webContents`.
Calling `event.preventDefault()` will make it throw an error.

### Event: 'remote-require'

Returns:
@@ -663,6 +663,15 @@ Returns:
Emitted when the associated window logs a console message. Will not be emitted
for windows with *offscreen rendering* enabled.

#### Event: 'desktop-capturer-get-sources'

Returns:

* `event` Event

Emitted when `desktopCapturer.getSources()` is called in the renderer process.
Calling `event.preventDefault()` will make it throw an error.

#### Event: 'remote-require'

Returns:
@@ -368,6 +368,10 @@ WebContents.prototype._init = function () {
})
})

this.on('desktop-capturer-get-sources', (event, ...args) => {
app.emit('desktop-capturer-get-sources', event, this, ...args)
})

this.on('remote-require', (event, ...args) => {
app.emit('remote-require', event, this, ...args)
})
@@ -1,7 +1,10 @@
'use strict'

const ipcMain = require('@electron/internal/browser/ipc-main-internal')
const errorUtils = require('@electron/internal/common/error-utils')

const { desktopCapturer } = process.atomBinding('desktop_capturer')
const eventBinding = process.atomBinding('event')

const deepEqual = (a, b) => JSON.stringify(a) === JSON.stringify(b)

@@ -12,6 +15,15 @@ const electronSources = 'ELECTRON_BROWSER_DESKTOP_CAPTURER_GET_SOURCES'
const capturerResult = (id) => `ELECTRON_RENDERER_DESKTOP_CAPTURER_RESULT_${id}`

ipcMain.on(electronSources, (event, captureWindow, captureScreen, thumbnailSize, fetchWindowIcons, id) => {
const customEvent = eventBinding.createWithSender(event.sender)
event.sender.emit('desktop-capturer-get-sources', customEvent)

if (customEvent.defaultPrevented) {
const error = new Error('desktopCapturer.getSources() blocked')
event.sender._sendInternal(capturerResult(id), errorUtils.serialize(error))
return
}

const request = {
id,
options: {
@@ -51,15 +63,15 @@ desktopCapturer.emit = (event, name, sources, fetchWindowIcons) => {
})

if (handledWebContents) {
handledWebContents._sendInternal(capturerResult(handledRequest.id), result)
handledWebContents._sendInternal(capturerResult(handledRequest.id), null, result)
}

// Check the queue to see whether there is another identical request & handle
requestsQueue.forEach(request => {
const webContents = request.webContents
if (deepEqual(handledRequest.options, request.options)) {
if (webContents) {
webContents._sendInternal(capturerResult(request.id), result)
webContents._sendInternal(capturerResult(request.id), null, result)
}
} else {
unhandledRequestsQueue.push(request)
@@ -1,7 +1,9 @@
'use strict'

const { nativeImage } = require('electron')

const ipcRenderer = require('@electron/internal/renderer/ipc-renderer-internal')
const errorUtils = require('@electron/internal/common/error-utils')

const includes = [].includes
let currentId = 0
@@ -17,6 +19,16 @@ function isValid (options) {
return Array.isArray(types)
}

function mapSources (sources) {
return sources.map(source => ({
id: source.id,
name: source.name,
thumbnail: nativeImage.createFromDataURL(source.thumbnail),
display_id: source.display_id,
appIcon: source.appIcon ? nativeImage.createFromDataURL(source.appIcon) : null
}))
}

exports.getSources = function (options, callback) {
if (!isValid(options)) return callback(new Error('Invalid options'))
const captureWindow = includes.call(options.types, 'window')
@@ -34,19 +46,11 @@ exports.getSources = function (options, callback) {

const id = incrementId()
ipcRenderer.send('ELECTRON_BROWSER_DESKTOP_CAPTURER_GET_SOURCES', captureWindow, captureScreen, options.thumbnailSize, options.fetchWindowIcons, id)
return ipcRenderer.once(`ELECTRON_RENDERER_DESKTOP_CAPTURER_RESULT_${id}`, (event, sources) => {
callback(null, (() => {
const results = []
sources.forEach(source => {
results.push({
id: source.id,
name: source.name,
thumbnail: nativeImage.createFromDataURL(source.thumbnail),
display_id: source.display_id,
appIcon: source.appIcon ? nativeImage.createFromDataURL(source.appIcon) : null
})
})
return results
})())
return ipcRenderer.once(`ELECTRON_RENDERER_DESKTOP_CAPTURER_RESULT_${id}`, (event, error, sources) => {
if (error) {
callback(errorUtils.deserialize(error))
} else {
callback(null, mapSources(sources))
}
})
}
@@ -370,6 +370,16 @@ describe('app module', () => {
w = new BrowserWindow({ show: false })
})

it('should emit desktop-capturer-get-sources event when desktopCapturer.getSources() is invoked', (done) => {
app.once('desktop-capturer-get-sources', (event, webContents) => {
expect(webContents).to.equal(w.webContents)
done()
})
w = new BrowserWindow({ show: false })
w.loadURL('about:blank')
w.webContents.executeJavaScript(`require('electron').desktopCapturer.getSources({ types: ['screen'] }, () => {})`)
})

it('should emit remote-require event when remote.require() is invoked', (done) => {
app.once('remote-require', (event, webContents, moduleName) => {
expect(webContents).to.equal(w.webContents)
@@ -1,6 +1,6 @@
const chai = require('chai')
const dirtyChai = require('dirty-chai')
const { desktopCapturer, remote } = require('electron')
const { desktopCapturer, ipcRenderer, remote } = require('electron')
const { screen } = remote
const features = process.atomBinding('features')

@@ -39,6 +39,15 @@ describe('desktopCapturer', () => {
})
})

it('throws an error when blocked', done => {
ipcRenderer.send('handle-next-desktop-capturer-get-sources')
desktopCapturer.getSources({ types: ['screen'] }, (error, sources) => {
expect(error.message).to.equal('desktopCapturer.getSources() blocked')
expect(sources).to.be.undefined()
done()
})
})

it('does not throw an error when called more than once (regression)', (done) => {
let callCount = 0
const callback = (error, sources) => {
@@ -233,6 +233,12 @@ app.on('ready', function () {
})
})

ipcMain.on('handle-next-desktop-capturer-get-sources', function (event) {
event.sender.once('desktop-capturer-get-sources', (event) => {
event.preventDefault()
})
})

ipcMain.on('handle-next-remote-require', function (event, modulesMap = {}) {
event.sender.once('remote-require', (event, moduleName) => {
event.preventDefault()

0 comments on commit fe1397e

Please sign in to comment.
You can’t perform that action at this time.