fix: security: don't allow arbitrary methods to be invoked on webContents via IPC (#15919)
This commit is contained in:
parent
0a23c0b032
commit
aa2b2f7c8f
7 changed files with 115 additions and 90 deletions
|
@ -62,6 +62,7 @@ filenames = {
|
||||||
"lib/common/init.js",
|
"lib/common/init.js",
|
||||||
"lib/common/parse-features-string.js",
|
"lib/common/parse-features-string.js",
|
||||||
"lib/common/reset-search-paths.js",
|
"lib/common/reset-search-paths.js",
|
||||||
|
"lib/common/web-view-methods.js",
|
||||||
"lib/renderer/callbacks-registry.js",
|
"lib/renderer/callbacks-registry.js",
|
||||||
"lib/renderer/chrome-api.js",
|
"lib/renderer/chrome-api.js",
|
||||||
"lib/renderer/content-scripts-injector.js",
|
"lib/renderer/content-scripts-injector.js",
|
||||||
|
|
|
@ -4,6 +4,7 @@ const { webContents } = require('electron')
|
||||||
const ipcMain = require('@electron/internal/browser/ipc-main-internal')
|
const ipcMain = require('@electron/internal/browser/ipc-main-internal')
|
||||||
const parseFeaturesString = require('@electron/internal/common/parse-features-string')
|
const parseFeaturesString = require('@electron/internal/common/parse-features-string')
|
||||||
const errorUtils = require('@electron/internal/common/error-utils')
|
const errorUtils = require('@electron/internal/common/error-utils')
|
||||||
|
const { syncMethods, asyncMethods } = require('@electron/internal/common/web-view-methods')
|
||||||
|
|
||||||
// Doesn't exist in early initialization.
|
// Doesn't exist in early initialization.
|
||||||
let webViewManager = null
|
let webViewManager = null
|
||||||
|
@ -368,7 +369,10 @@ handleMessage('ELECTRON_GUEST_VIEW_MANAGER_ASYNC_CALL', function (event, request
|
||||||
new Promise(resolve => {
|
new Promise(resolve => {
|
||||||
const guest = getGuest(guestInstanceId)
|
const guest = getGuest(guestInstanceId)
|
||||||
if (guest.hostWebContents !== event.sender) {
|
if (guest.hostWebContents !== event.sender) {
|
||||||
throw new Error('Access denied')
|
throw new Error(`Invalid guestInstanceId: ${guestInstanceId}`)
|
||||||
|
}
|
||||||
|
if (!asyncMethods.has(method)) {
|
||||||
|
throw new Error(`Invalid method: ${method}`)
|
||||||
}
|
}
|
||||||
if (hasCallback) {
|
if (hasCallback) {
|
||||||
guest[method](...args, resolve)
|
guest[method](...args, resolve)
|
||||||
|
@ -388,9 +392,12 @@ handleMessage('ELECTRON_GUEST_VIEW_MANAGER_SYNC_CALL', function (event, guestIns
|
||||||
try {
|
try {
|
||||||
const guest = getGuest(guestInstanceId)
|
const guest = getGuest(guestInstanceId)
|
||||||
if (guest.hostWebContents !== event.sender) {
|
if (guest.hostWebContents !== event.sender) {
|
||||||
throw new Error('Access denied')
|
throw new Error(`Invalid guestInstanceId: ${guestInstanceId}`)
|
||||||
}
|
}
|
||||||
event.returnValue = [null, guest[method].apply(guest, args)]
|
if (!syncMethods.has(method)) {
|
||||||
|
throw new Error(`Invalid method: ${method}`)
|
||||||
|
}
|
||||||
|
event.returnValue = [null, guest[method](...args)]
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
event.returnValue = [errorUtils.serialize(error)]
|
event.returnValue = [errorUtils.serialize(error)]
|
||||||
}
|
}
|
||||||
|
|
|
@ -288,6 +288,11 @@ ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_CLOSE', function (event, guestI
|
||||||
if (guestWindow != null) guestWindow.destroy()
|
if (guestWindow != null) guestWindow.destroy()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const windowMethods = new Set([
|
||||||
|
'focus',
|
||||||
|
'blur'
|
||||||
|
])
|
||||||
|
|
||||||
ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_METHOD', function (event, guestId, method, ...args) {
|
ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_METHOD', function (event, guestId, method, ...args) {
|
||||||
const guestContents = webContents.fromId(guestId)
|
const guestContents = webContents.fromId(guestId)
|
||||||
if (guestContents == null) {
|
if (guestContents == null) {
|
||||||
|
@ -295,7 +300,7 @@ ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_METHOD', function (event, guest
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!canAccessWindow(event.sender, guestContents)) {
|
if (!canAccessWindow(event.sender, guestContents) || !windowMethods.has(method)) {
|
||||||
console.error(`Blocked ${event.sender.getURL()} from calling ${method} on its opener.`)
|
console.error(`Blocked ${event.sender.getURL()} from calling ${method} on its opener.`)
|
||||||
event.returnValue = null
|
event.returnValue = null
|
||||||
return
|
return
|
||||||
|
@ -326,17 +331,27 @@ ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_POSTMESSAGE', function (event,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const webContentsMethods = new Set([
|
||||||
|
'print',
|
||||||
|
'executeJavaScript'
|
||||||
|
])
|
||||||
|
|
||||||
ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD', function (event, guestId, method, ...args) {
|
ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD', function (event, guestId, method, ...args) {
|
||||||
const guestContents = webContents.fromId(guestId)
|
const guestContents = webContents.fromId(guestId)
|
||||||
if (guestContents == null) return
|
if (guestContents == null) return
|
||||||
|
|
||||||
if (canAccessWindow(event.sender, guestContents)) {
|
if (canAccessWindow(event.sender, guestContents) && webContentsMethods.has(method)) {
|
||||||
guestContents[method](...args)
|
guestContents[method](...args)
|
||||||
} else {
|
} else {
|
||||||
console.error(`Blocked ${event.sender.getURL()} from calling ${method} on its opener.`)
|
console.error(`Blocked ${event.sender.getURL()} from calling ${method} on its opener.`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const webContentsSyncMethods = new Set([
|
||||||
|
'getURL',
|
||||||
|
'loadURL'
|
||||||
|
])
|
||||||
|
|
||||||
ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD_SYNC', function (event, guestId, method, ...args) {
|
ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD_SYNC', function (event, guestId, method, ...args) {
|
||||||
const guestContents = webContents.fromId(guestId)
|
const guestContents = webContents.fromId(guestId)
|
||||||
if (guestContents == null) {
|
if (guestContents == null) {
|
||||||
|
@ -344,7 +359,7 @@ ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD_SYNC', function (e
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canAccessWindow(event.sender, guestContents)) {
|
if (canAccessWindow(event.sender, guestContents) && webContentsSyncMethods.has(method)) {
|
||||||
event.returnValue = guestContents[method](...args)
|
event.returnValue = guestContents[method](...args)
|
||||||
} else {
|
} else {
|
||||||
console.error(`Blocked ${event.sender.getURL()} from calling ${method} on its opener.`)
|
console.error(`Blocked ${event.sender.getURL()} from calling ${method} on its opener.`)
|
||||||
|
|
|
@ -3,12 +3,20 @@
|
||||||
const ipcMain = require('@electron/internal/browser/ipc-main-internal')
|
const ipcMain = require('@electron/internal/browser/ipc-main-internal')
|
||||||
|
|
||||||
// The history operation in renderer is redirected to browser.
|
// The history operation in renderer is redirected to browser.
|
||||||
ipcMain.on('ELECTRON_NAVIGATION_CONTROLLER', function (event, method, ...args) {
|
ipcMain.on('ELECTRON_NAVIGATION_CONTROLLER_GO_BACK', function (event) {
|
||||||
event.sender[method](...args)
|
event.sender.goBack()
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.on('ELECTRON_SYNC_NAVIGATION_CONTROLLER', function (event, method, ...args) {
|
ipcMain.on('ELECTRON_NAVIGATION_CONTROLLER_GO_FORWARD', function (event) {
|
||||||
event.returnValue = event.sender[method](...args)
|
event.sender.goForward()
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.on('ELECTRON_NAVIGATION_CONTROLLER_GO_TO_OFFSET', function (event, offset) {
|
||||||
|
event.sender.goToOffset(offset)
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.on('ELECTRON_NAVIGATION_CONTROLLER_LENGTH', function (event) {
|
||||||
|
event.returnValue = event.sender.length()
|
||||||
})
|
})
|
||||||
|
|
||||||
// JavaScript implementation of Chromium's NavigationController.
|
// JavaScript implementation of Chromium's NavigationController.
|
||||||
|
|
67
lib/common/web-view-methods.js
Normal file
67
lib/common/web-view-methods.js
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
// Public-facing API methods.
|
||||||
|
exports.syncMethods = new Set([
|
||||||
|
'getURL',
|
||||||
|
'loadURL',
|
||||||
|
'getTitle',
|
||||||
|
'isLoading',
|
||||||
|
'isLoadingMainFrame',
|
||||||
|
'isWaitingForResponse',
|
||||||
|
'stop',
|
||||||
|
'reload',
|
||||||
|
'reloadIgnoringCache',
|
||||||
|
'canGoBack',
|
||||||
|
'canGoForward',
|
||||||
|
'canGoToOffset',
|
||||||
|
'clearHistory',
|
||||||
|
'goBack',
|
||||||
|
'goForward',
|
||||||
|
'goToIndex',
|
||||||
|
'goToOffset',
|
||||||
|
'isCrashed',
|
||||||
|
'setUserAgent',
|
||||||
|
'getUserAgent',
|
||||||
|
'openDevTools',
|
||||||
|
'closeDevTools',
|
||||||
|
'isDevToolsOpened',
|
||||||
|
'isDevToolsFocused',
|
||||||
|
'inspectElement',
|
||||||
|
'setAudioMuted',
|
||||||
|
'isAudioMuted',
|
||||||
|
'isCurrentlyAudible',
|
||||||
|
'undo',
|
||||||
|
'redo',
|
||||||
|
'cut',
|
||||||
|
'copy',
|
||||||
|
'paste',
|
||||||
|
'pasteAndMatchStyle',
|
||||||
|
'delete',
|
||||||
|
'selectAll',
|
||||||
|
'unselect',
|
||||||
|
'replace',
|
||||||
|
'replaceMisspelling',
|
||||||
|
'findInPage',
|
||||||
|
'stopFindInPage',
|
||||||
|
'downloadURL',
|
||||||
|
'inspectServiceWorker',
|
||||||
|
'showDefinitionForSelection',
|
||||||
|
'setZoomFactor',
|
||||||
|
'setZoomLevel'
|
||||||
|
])
|
||||||
|
|
||||||
|
exports.asyncMethods = new Set([
|
||||||
|
'insertCSS',
|
||||||
|
'insertText',
|
||||||
|
'send',
|
||||||
|
'sendInputEvent',
|
||||||
|
'setLayoutZoomLevelLimits',
|
||||||
|
'setVisualZoomLevelLimits',
|
||||||
|
// with callback
|
||||||
|
'capturePage',
|
||||||
|
'executeJavaScript',
|
||||||
|
'getZoomFactor',
|
||||||
|
'getZoomLevel',
|
||||||
|
'print',
|
||||||
|
'printToPDF'
|
||||||
|
])
|
|
@ -7,6 +7,7 @@ const ipcRenderer = require('@electron/internal/renderer/ipc-renderer-internal')
|
||||||
const guestViewInternal = require('@electron/internal/renderer/web-view/guest-view-internal')
|
const guestViewInternal = require('@electron/internal/renderer/web-view/guest-view-internal')
|
||||||
const webViewConstants = require('@electron/internal/renderer/web-view/web-view-constants')
|
const webViewConstants = require('@electron/internal/renderer/web-view/web-view-constants')
|
||||||
const errorUtils = require('@electron/internal/common/error-utils')
|
const errorUtils = require('@electron/internal/common/error-utils')
|
||||||
|
const { syncMethods, asyncMethods } = require('@electron/internal/common/web-view-methods')
|
||||||
|
|
||||||
// ID generator.
|
// ID generator.
|
||||||
let nextId = 0
|
let nextId = 0
|
||||||
|
@ -230,71 +231,6 @@ const registerWebViewElement = function () {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Public-facing API methods.
|
|
||||||
const methods = [
|
|
||||||
'getURL',
|
|
||||||
'loadURL',
|
|
||||||
'getTitle',
|
|
||||||
'isLoading',
|
|
||||||
'isLoadingMainFrame',
|
|
||||||
'isWaitingForResponse',
|
|
||||||
'stop',
|
|
||||||
'reload',
|
|
||||||
'reloadIgnoringCache',
|
|
||||||
'canGoBack',
|
|
||||||
'canGoForward',
|
|
||||||
'canGoToOffset',
|
|
||||||
'clearHistory',
|
|
||||||
'goBack',
|
|
||||||
'goForward',
|
|
||||||
'goToIndex',
|
|
||||||
'goToOffset',
|
|
||||||
'isCrashed',
|
|
||||||
'setUserAgent',
|
|
||||||
'getUserAgent',
|
|
||||||
'openDevTools',
|
|
||||||
'closeDevTools',
|
|
||||||
'isDevToolsOpened',
|
|
||||||
'isDevToolsFocused',
|
|
||||||
'inspectElement',
|
|
||||||
'setAudioMuted',
|
|
||||||
'isAudioMuted',
|
|
||||||
'isCurrentlyAudible',
|
|
||||||
'undo',
|
|
||||||
'redo',
|
|
||||||
'cut',
|
|
||||||
'copy',
|
|
||||||
'paste',
|
|
||||||
'pasteAndMatchStyle',
|
|
||||||
'delete',
|
|
||||||
'selectAll',
|
|
||||||
'unselect',
|
|
||||||
'replace',
|
|
||||||
'replaceMisspelling',
|
|
||||||
'findInPage',
|
|
||||||
'stopFindInPage',
|
|
||||||
'downloadURL',
|
|
||||||
'inspectServiceWorker',
|
|
||||||
'showDefinitionForSelection',
|
|
||||||
'setZoomFactor',
|
|
||||||
'setZoomLevel'
|
|
||||||
]
|
|
||||||
const nonblockMethods = [
|
|
||||||
'insertCSS',
|
|
||||||
'insertText',
|
|
||||||
'send',
|
|
||||||
'sendInputEvent',
|
|
||||||
'setLayoutZoomLevelLimits',
|
|
||||||
'setVisualZoomLevelLimits',
|
|
||||||
// with callback
|
|
||||||
'capturePage',
|
|
||||||
'executeJavaScript',
|
|
||||||
'getZoomFactor',
|
|
||||||
'getZoomLevel',
|
|
||||||
'print',
|
|
||||||
'printToPDF'
|
|
||||||
]
|
|
||||||
|
|
||||||
const getGuestInstanceId = function (self) {
|
const getGuestInstanceId = function (self) {
|
||||||
const internal = v8Util.getHiddenValue(self, 'internal')
|
const internal = v8Util.getHiddenValue(self, 'internal')
|
||||||
if (!internal.guestInstanceId) {
|
if (!internal.guestInstanceId) {
|
||||||
|
@ -314,7 +250,7 @@ const registerWebViewElement = function () {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const method of methods) {
|
for (const method of syncMethods) {
|
||||||
proto[method] = createBlockHandler(method)
|
proto[method] = createBlockHandler(method)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -332,7 +268,7 @@ const registerWebViewElement = function () {
|
||||||
ipcRenderer.send('ELECTRON_GUEST_VIEW_MANAGER_ASYNC_CALL', requestId, getGuestInstanceId(this), method, args, callback != null)
|
ipcRenderer.send('ELECTRON_GUEST_VIEW_MANAGER_ASYNC_CALL', requestId, getGuestInstanceId(this), method, args, callback != null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const method of nonblockMethods) {
|
for (const method of asyncMethods) {
|
||||||
proto[method] = createNonBlockHandler(method)
|
proto[method] = createNonBlockHandler(method)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -146,15 +146,6 @@ function BrowserWindowProxy (ipcRenderer, guestId) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Forward history operations to browser.
|
|
||||||
const sendHistoryOperation = function (ipcRenderer, ...args) {
|
|
||||||
ipcRenderer.send('ELECTRON_NAVIGATION_CONTROLLER', ...args)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getHistoryOperation = function (ipcRenderer, ...args) {
|
|
||||||
return ipcRenderer.sendSync('ELECTRON_SYNC_NAVIGATION_CONTROLLER', ...args)
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = (ipcRenderer, guestInstanceId, openerId, hiddenPage, usesNativeWindowOpen) => {
|
module.exports = (ipcRenderer, guestInstanceId, openerId, hiddenPage, usesNativeWindowOpen) => {
|
||||||
if (guestInstanceId == null) {
|
if (guestInstanceId == null) {
|
||||||
// Override default window.close.
|
// Override default window.close.
|
||||||
|
@ -199,20 +190,20 @@ module.exports = (ipcRenderer, guestInstanceId, openerId, hiddenPage, usesNative
|
||||||
})
|
})
|
||||||
|
|
||||||
window.history.back = function () {
|
window.history.back = function () {
|
||||||
sendHistoryOperation(ipcRenderer, 'goBack')
|
ipcRenderer.send('ELECTRON_NAVIGATION_CONTROLLER_GO_BACK')
|
||||||
}
|
}
|
||||||
|
|
||||||
window.history.forward = function () {
|
window.history.forward = function () {
|
||||||
sendHistoryOperation(ipcRenderer, 'goForward')
|
ipcRenderer.send('ELECTRON_NAVIGATION_CONTROLLER_GO_FORWARD')
|
||||||
}
|
}
|
||||||
|
|
||||||
window.history.go = function (offset) {
|
window.history.go = function (offset) {
|
||||||
sendHistoryOperation(ipcRenderer, 'goToOffset', +offset)
|
ipcRenderer.send('ELECTRON_NAVIGATION_CONTROLLER_GO_TO_OFFSET', +offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
defineProperty(window.history, 'length', {
|
defineProperty(window.history, 'length', {
|
||||||
get: function () {
|
get: function () {
|
||||||
return getHistoryOperation(ipcRenderer, 'length')
|
return ipcRenderer.sendSync('ELECTRON_NAVIGATION_CONTROLLER_LENGTH')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue