feat: only allow bundled preload scripts (#17308)
This commit is contained in:
parent
3d307e5610
commit
8cf15cc931
11 changed files with 79 additions and 3 deletions
|
@ -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 })`
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -561,6 +561,8 @@ Returns `Promise<void>` - resolves when the session’s 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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
6
lib/common/path-utils.ts
Normal 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)
|
||||||
|
}
|
|
@ -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}`)
|
||||||
|
|
|
@ -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')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue