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…
	
	Add table
		Add a link
		
	
		Reference in a new issue