feat: only allow bundled preload scripts (#17308)

This commit is contained in:
Milan Burda 2019-03-28 11:38:51 +01:00 committed by Alexey Kuzmin
parent 3d307e5610
commit 8cf15cc931
11 changed files with 79 additions and 3 deletions

View file

@ -81,6 +81,10 @@ powerMonitor.querySystemIdleTime(callback)
const idleTime = getSystemIdleTime() const idleTime = getSystemIdleTime()
``` ```
## Preload scripts outside of app path are not allowed
For security reasons, preload scripts can only be loaded from a subpath of the [app path](app.md#appgetapppath).
# Planned Breaking API Changes (5.0) # Planned Breaking API Changes (5.0)
## `new BrowserWindow({ webPreferences })` ## `new BrowserWindow({ webPreferences })`

View file

@ -266,6 +266,8 @@ It creates a new `BrowserWindow` with native properties as set by the `options`.
When node integration is turned off, the preload script can reintroduce When node integration is turned off, the preload script can reintroduce
Node global symbols back to the global scope. See example Node global symbols back to the global scope. See example
[here](process.md#event-loaded). [here](process.md#event-loaded).
**Note:** For security reasons, preload scripts can only be loaded from
a subpath of the [app path](app.md#appgetapppath).
* `sandbox` Boolean (optional) - If set, this will sandbox the renderer * `sandbox` Boolean (optional) - If set, this will sandbox the renderer
associated with the window, making it compatible with the Chromium associated with the window, making it compatible with the Chromium
OS-level sandbox and disabling the Node.js engine. This is not the same as OS-level sandbox and disabling the Node.js engine. This is not the same as

View file

@ -77,7 +77,7 @@ app.on('ready', () => {
win = new BrowserWindow({ win = new BrowserWindow({
webPreferences: { webPreferences: {
sandbox: true, sandbox: true,
preload: 'preload.js' preload: path.join(app.getAppPath(), 'preload.js')
} }
}) })
win.loadURL('http://google.com') win.loadURL('http://google.com')

View file

@ -561,6 +561,8 @@ Returns `Promise<void>` - resolves when the sessions HTTP authentication cach
Adds scripts that will be executed on ALL web contents that are associated with Adds scripts that will be executed on ALL web contents that are associated with
this session just before normal `preload` scripts run. this session just before normal `preload` scripts run.
**Note:** For security reasons, preload scripts can only be loaded from a subpath of the [app path](app.md#appgetapppath).
#### `ses.getPreloads()` #### `ses.getPreloads()`
Returns `String[]` an array of paths to preload scripts that have been Returns `String[]` an array of paths to preload scripts that have been

View file

@ -162,6 +162,9 @@ When the guest page doesn't have node integration this script will still have
access to all Node APIs, but global objects injected by Node will be deleted access to all Node APIs, but global objects injected by Node will be deleted
after this script has finished executing. after this script has finished executing.
**Note:** For security reasons, preload scripts can only be loaded from
a subpath of the [app path](app.md#appgetapppath).
**Note:** This option will be appear as `preloadURL` (not `preload`) in **Note:** This option will be appear as `preloadURL` (not `preload`) in
the `webPreferences` specified to the `will-attach-webview` event. the `webPreferences` specified to the `will-attach-webview` event.

View file

@ -193,7 +193,7 @@ const mainWindow = new BrowserWindow({
webPreferences: { webPreferences: {
nodeIntegration: false, nodeIntegration: false,
nodeIntegrationInWorker: false, nodeIntegrationInWorker: false,
preload: './preload.js' preload: path.join(app.getAppPath(), 'preload.js')
} }
}) })
@ -260,7 +260,7 @@ very small investment.
const mainWindow = new BrowserWindow({ const mainWindow = new BrowserWindow({
webPreferences: { webPreferences: {
contextIsolation: true, contextIsolation: true,
preload: 'preload.js' preload: path.join(app.getAppPath(), 'preload.js')
} }
}) })
``` ```

View file

@ -61,6 +61,7 @@ filenames = {
"lib/common/error-utils.js", "lib/common/error-utils.js",
"lib/common/init.ts", "lib/common/init.ts",
"lib/common/parse-features-string.js", "lib/common/parse-features-string.js",
"lib/common/path-utils.ts",
"lib/common/reset-search-paths.ts", "lib/common/reset-search-paths.ts",
"lib/common/web-view-methods.js", "lib/common/web-view-methods.js",
"lib/renderer/callbacks-registry.js", "lib/renderer/callbacks-registry.js",

View file

@ -3,6 +3,7 @@
const electron = require('electron') const electron = require('electron')
const { EventEmitter } = require('events') const { EventEmitter } = require('events')
const fs = require('fs') const fs = require('fs')
const path = require('path')
const util = require('util') const util = require('util')
const v8Util = process.electronBinding('v8_util') const v8Util = process.electronBinding('v8_util')
@ -19,6 +20,7 @@ const guestViewManager = require('@electron/internal/browser/guest-view-manager'
const bufferUtils = require('@electron/internal/common/buffer-utils') const bufferUtils = require('@electron/internal/common/buffer-utils')
const errorUtils = require('@electron/internal/common/error-utils') const errorUtils = require('@electron/internal/common/error-utils')
const clipboardUtils = require('@electron/internal/common/clipboard-utils') const clipboardUtils = require('@electron/internal/common/clipboard-utils')
const { isParentDir } = require('@electron/internal/common/path-utils')
const hasProp = {}.hasOwnProperty const hasProp = {}.hasOwnProperty
@ -498,12 +500,24 @@ ipcMainUtils.handle('ELECTRON_BROWSER_CLIPBOARD', function (event, method, ...ar
}) })
const readFile = util.promisify(fs.readFile) const readFile = util.promisify(fs.readFile)
const realpath = util.promisify(fs.realpath)
let absoluteAppPath
const getAppPath = async function () {
if (absoluteAppPath === undefined) {
absoluteAppPath = await realpath(electron.app.getAppPath())
}
return absoluteAppPath
}
const getPreloadScript = async function (preloadPath) { const getPreloadScript = async function (preloadPath) {
let preloadSrc = null let preloadSrc = null
let preloadError = null let preloadError = null
if (preloadPath) { if (preloadPath) {
try { try {
if (!isParentDir(await getAppPath(), await realpath(preloadPath))) {
throw new Error('Preload scripts outside of app path are not allowed')
}
preloadSrc = (await readFile(preloadPath)).toString() preloadSrc = (await readFile(preloadPath)).toString()
} catch (err) { } catch (err) {
preloadError = errorUtils.serialize(err) preloadError = errorUtils.serialize(err)

6
lib/common/path-utils.ts Normal file
View file

@ -0,0 +1,6 @@
import * as path from 'path'
export const isParentDir = function (parent: string, dir: string) {
const relative = path.relative(parent, dir)
return !!relative && !relative.startsWith('..') && !path.isAbsolute(relative)
}

View file

@ -1,4 +1,5 @@
import { EventEmitter } from 'events' import { EventEmitter } from 'events'
import * as fs from 'fs'
import * as path from 'path' import * as path from 'path'
const Module = require('module') const Module = require('module')
@ -160,10 +161,22 @@ if (nodeIntegration) {
} }
const errorUtils = require('@electron/internal/common/error-utils') const errorUtils = require('@electron/internal/common/error-utils')
const { isParentDir } = require('@electron/internal/common/path-utils')
let absoluteAppPath: string
const getAppPath = function () {
if (absoluteAppPath === undefined) {
absoluteAppPath = fs.realpathSync(appPath!)
}
return absoluteAppPath
}
// Load the preload scripts. // Load the preload scripts.
for (const preloadScript of preloadScripts) { for (const preloadScript of preloadScripts) {
try { try {
if (!isParentDir(getAppPath(), fs.realpathSync(preloadScript))) {
throw new Error('Preload scripts outside of app path are not allowed')
}
require(preloadScript) require(preloadScript)
} catch (error) { } catch (error) {
console.error(`Unable to load preload script: ${preloadScript}`) console.error(`Unable to load preload script: ${preloadScript}`)

View file

@ -3,6 +3,7 @@
const assert = require('assert') const assert = require('assert')
const ChildProcess = require('child_process') const ChildProcess = require('child_process')
const fs = require('fs') const fs = require('fs')
const os = require('os')
const http = require('http') const http = require('http')
const path = require('path') const path = require('path')
const { closeWindow } = require('./window-helpers') const { closeWindow } = require('./window-helpers')
@ -1110,6 +1111,16 @@ describe('webContents module', () => {
describe('preload-error event', () => { describe('preload-error event', () => {
const generateSpecs = (description, sandbox) => { const generateSpecs = (description, sandbox) => {
describe(description, () => { describe(description, () => {
const tmpPreload = path.join(os.tmpdir(), 'preload.js')
before((done) => {
fs.writeFile(tmpPreload, '', done)
})
after((done) => {
fs.unlink(tmpPreload, () => done())
})
it('is triggered when unhandled exception is thrown', async () => { it('is triggered when unhandled exception is thrown', async () => {
const preload = path.join(fixtures, 'module', 'preload-error-exception.js') const preload = path.join(fixtures, 'module', 'preload-error-exception.js')
@ -1169,6 +1180,26 @@ describe('webContents module', () => {
expect(preloadPath).to.equal(preload) expect(preloadPath).to.equal(preload)
expect(error.message).to.contain('preload-invalid.js') expect(error.message).to.contain('preload-invalid.js')
}) })
it('is triggered when preload script is outside of app path', async () => {
const preload = tmpPreload
w.destroy()
w = new BrowserWindow({
show: false,
webPreferences: {
sandbox,
preload
}
})
const promise = emittedOnce(w.webContents, 'preload-error')
w.loadURL('about:blank')
const [, preloadPath, error] = await promise
expect(preloadPath).to.equal(preload)
expect(error.message).to.contain('Preload scripts outside of app path are not allowed')
})
}) })
} }