chore: increase security of default_app (#17318)
This commit is contained in:
parent
188d31132b
commit
a8698d092b
8 changed files with 142 additions and 97 deletions
|
@ -1,4 +1,4 @@
|
||||||
import { app, BrowserWindow, BrowserWindowConstructorOptions } from 'electron'
|
import { app, dialog, BrowserWindow, shell, ipcMain } from 'electron'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
|
|
||||||
let mainWindow: BrowserWindow | null = null
|
let mainWindow: BrowserWindow | null = null
|
||||||
|
@ -8,18 +8,52 @@ app.on('window-all-closed', () => {
|
||||||
app.quit()
|
app.quit()
|
||||||
})
|
})
|
||||||
|
|
||||||
export const load = async (appUrl: string) => {
|
function decorateURL (url: string) {
|
||||||
|
// safely add `?utm_source=default_app
|
||||||
|
const parsedUrl = new URL(url)
|
||||||
|
parsedUrl.searchParams.append('utm_source', 'default_app')
|
||||||
|
return parsedUrl.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the shortest path to the electron binary
|
||||||
|
const absoluteElectronPath = process.execPath
|
||||||
|
const relativeElectronPath = path.relative(process.cwd(), absoluteElectronPath)
|
||||||
|
const electronPath = absoluteElectronPath.length < relativeElectronPath.length
|
||||||
|
? absoluteElectronPath
|
||||||
|
: relativeElectronPath
|
||||||
|
|
||||||
|
const indexPath = path.resolve(app.getAppPath(), 'index.html')
|
||||||
|
|
||||||
|
function isTrustedSender (webContents: Electron.WebContents) {
|
||||||
|
if (webContents !== (mainWindow && mainWindow.webContents)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedUrl = new URL(webContents.getURL())
|
||||||
|
return parsedUrl.protocol === 'file:' && parsedUrl.pathname === indexPath
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcMain.on('bootstrap', (event) => {
|
||||||
|
try {
|
||||||
|
event.returnValue = isTrustedSender(event.sender) ? electronPath : null
|
||||||
|
} catch {
|
||||||
|
event.returnValue = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function createWindow () {
|
||||||
await app.whenReady()
|
await app.whenReady()
|
||||||
|
|
||||||
const options: BrowserWindowConstructorOptions = {
|
const options: Electron.BrowserWindowConstructorOptions = {
|
||||||
width: 900,
|
width: 900,
|
||||||
height: 600,
|
height: 600,
|
||||||
autoHideMenuBar: true,
|
autoHideMenuBar: true,
|
||||||
backgroundColor: '#FFFFFF',
|
backgroundColor: '#FFFFFF',
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
|
preload: path.resolve(__dirname, 'preload.js'),
|
||||||
contextIsolation: true,
|
contextIsolation: true,
|
||||||
preload: path.resolve(__dirname, 'renderer.js'),
|
sandbox: true,
|
||||||
webviewTag: false
|
enableRemoteModule: false
|
||||||
},
|
},
|
||||||
useContentSize: true,
|
useContentSize: true,
|
||||||
show: false
|
show: false
|
||||||
|
@ -30,9 +64,39 @@ export const load = async (appUrl: string) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
mainWindow = new BrowserWindow(options)
|
mainWindow = new BrowserWindow(options)
|
||||||
|
|
||||||
mainWindow.on('ready-to-show', () => mainWindow!.show())
|
mainWindow.on('ready-to-show', () => mainWindow!.show())
|
||||||
|
|
||||||
|
mainWindow.webContents.on('new-window', (event, url) => {
|
||||||
|
event.preventDefault()
|
||||||
|
shell.openExternal(decorateURL(url))
|
||||||
|
})
|
||||||
|
|
||||||
|
mainWindow.webContents.session.setPermissionRequestHandler((webContents, permission, done) => {
|
||||||
|
const parsedUrl = new URL(webContents.getURL())
|
||||||
|
|
||||||
|
const options: Electron.MessageBoxOptions = {
|
||||||
|
title: 'Permission Request',
|
||||||
|
message: `Allow '${parsedUrl.origin}' to access '${permission}'?`,
|
||||||
|
buttons: ['OK', 'Cancel'],
|
||||||
|
cancelId: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.showMessageBox(mainWindow!, options, (response) => {
|
||||||
|
done(response === 0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return mainWindow
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loadURL = async (appUrl: string) => {
|
||||||
|
mainWindow = await createWindow()
|
||||||
mainWindow.loadURL(appUrl)
|
mainWindow.loadURL(appUrl)
|
||||||
mainWindow.focus()
|
mainWindow.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const loadFile = async (appPath: string) => {
|
||||||
|
mainWindow = await createWindow()
|
||||||
|
mainWindow.loadFile(appPath)
|
||||||
|
mainWindow.focus()
|
||||||
|
}
|
||||||
|
|
|
@ -2,9 +2,10 @@
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<title>Electron</title>
|
<title>Electron</title>
|
||||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self'" />
|
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self'; connect-src 'self'" />
|
||||||
<link href="./styles.css" type="text/css" rel="stylesheet" />
|
<link href="./styles.css" type="text/css" rel="stylesheet" />
|
||||||
<link href="./octicon/build.css" type="text/css" rel="stylesheet" />
|
<link href="./octicon/build.css" type="text/css" rel="stylesheet" />
|
||||||
|
<script defer src="./index.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
@ -52,31 +53,31 @@
|
||||||
|
|
||||||
<nav>
|
<nav>
|
||||||
<div class="linkcol">
|
<div class="linkcol">
|
||||||
<a class="hero-link" href="https://electronjs.org/blog">
|
<a class="hero-link" target="_blank" href="https://electronjs.org/blog">
|
||||||
<span class="octicon hero-octicon octicon-gist" aria-hidden="true"></span>
|
<span class="octicon hero-octicon octicon-gist" aria-hidden="true"></span>
|
||||||
<h4>Blog</h4>
|
<h4>Blog</h4>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="linkcol">
|
<div class="linkcol">
|
||||||
<a class="hero-link" href="https://github.com/electron/electron">
|
<a class="hero-link" target="_blank" href="https://github.com/electron/electron">
|
||||||
<span class="octicon hero-octicon octicon-mark-github" aria-hidden="true"></span>
|
<span class="octicon hero-octicon octicon-mark-github" aria-hidden="true"></span>
|
||||||
<h4>Repository</h4>
|
<h4>Repository</h4>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="linkcol">
|
<div class="linkcol">
|
||||||
<a class="hero-link" href="https://electronjs.org/docs">
|
<a class="hero-link" target="_blank" href="https://electronjs.org/docs">
|
||||||
<span class="octicon hero-octicon octicon-gear" aria-hidden="true"></span>
|
<span class="octicon hero-octicon octicon-gear" aria-hidden="true"></span>
|
||||||
<h4>Docs</h4>
|
<h4>Docs</h4>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="linkcol">
|
<div class="linkcol">
|
||||||
<a class="hero-link" href="https://github.com/electron/electron-api-demos">
|
<a class="hero-link" target="_blank" href="https://github.com/electron/electron-api-demos">
|
||||||
<span class="octicon hero-octicon octicon-star" aria-hidden="true"></span>
|
<span class="octicon hero-octicon octicon-star" aria-hidden="true"></span>
|
||||||
<h4>API Demos</h4>
|
<h4>API Demos</h4>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="linkcol">
|
<div class="linkcol">
|
||||||
<a class="hero-link" href="https://electronforge.io">
|
<a class="hero-link" target="_blank" href="https://electronforge.io">
|
||||||
<span class="octicon hero-octicon octicon-gift" aria-hidden="true"></span>
|
<span class="octicon hero-octicon octicon-gift" aria-hidden="true"></span>
|
||||||
<h4>Forge</h4>
|
<h4>Forge</h4>
|
||||||
</a>
|
</a>
|
||||||
|
|
30
default_app/index.ts
Normal file
30
default_app/index.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
async function getOcticonSvg (name: string) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`octicon/${name}.svg`)
|
||||||
|
const div = document.createElement('div')
|
||||||
|
div.innerHTML = await response.text()
|
||||||
|
return div
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSVG (element: HTMLSpanElement) {
|
||||||
|
for (const cssClass of element.classList) {
|
||||||
|
if (cssClass.startsWith('octicon-')) {
|
||||||
|
const icon = await getOcticonSvg(cssClass.substr(8))
|
||||||
|
if (icon) {
|
||||||
|
for (const elemClass of element.classList) {
|
||||||
|
icon.classList.add(elemClass)
|
||||||
|
}
|
||||||
|
element.before(icon)
|
||||||
|
element.remove()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const element of document.querySelectorAll<HTMLSpanElement>('.octicon')) {
|
||||||
|
loadSVG(element)
|
||||||
|
}
|
|
@ -129,9 +129,14 @@ function showErrorMessage (message: string) {
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadApplicationByUrl (appUrl: string) {
|
async function loadApplicationByURL (appUrl: string) {
|
||||||
const { load } = await import('./default_app')
|
const { loadURL } = await import('./default_app')
|
||||||
load(appUrl)
|
loadURL(appUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadApplicationByFile (appPath: string) {
|
||||||
|
const { loadFile } = await import('./default_app')
|
||||||
|
loadFile(appPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
function startRepl () {
|
function startRepl () {
|
||||||
|
@ -156,13 +161,9 @@ if (option.file && !option.webdriver) {
|
||||||
const protocol = url.parse(file).protocol
|
const protocol = url.parse(file).protocol
|
||||||
const extension = path.extname(file)
|
const extension = path.extname(file)
|
||||||
if (protocol === 'http:' || protocol === 'https:' || protocol === 'file:' || protocol === 'chrome:') {
|
if (protocol === 'http:' || protocol === 'https:' || protocol === 'file:' || protocol === 'chrome:') {
|
||||||
loadApplicationByUrl(file)
|
loadApplicationByURL(file)
|
||||||
} else if (extension === '.html' || extension === '.htm') {
|
} else if (extension === '.html' || extension === '.htm') {
|
||||||
loadApplicationByUrl(url.format({
|
loadApplicationByFile(path.resolve(file))
|
||||||
protocol: 'file:',
|
|
||||||
slashes: true,
|
|
||||||
pathname: path.resolve(file)
|
|
||||||
}))
|
|
||||||
} else {
|
} else {
|
||||||
loadApplicationPackage(file)
|
loadApplicationPackage(file)
|
||||||
}
|
}
|
||||||
|
@ -196,10 +197,5 @@ Options:
|
||||||
console.log(welcomeMessage)
|
console.log(welcomeMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
const indexPath = path.join(__dirname, '/index.html')
|
loadApplicationByFile('index.html')
|
||||||
loadApplicationByUrl(url.format({
|
|
||||||
protocol: 'file:',
|
|
||||||
slashes: true,
|
|
||||||
pathname: indexPath
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
20
default_app/preload.ts
Normal file
20
default_app/preload.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { ipcRenderer } from 'electron'
|
||||||
|
|
||||||
|
function initialize () {
|
||||||
|
const electronPath = ipcRenderer.sendSync('bootstrap')
|
||||||
|
|
||||||
|
function replaceText (selector: string, text: string) {
|
||||||
|
const element = document.querySelector<HTMLElement>(selector)
|
||||||
|
if (element) {
|
||||||
|
element.innerText = text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceText('.electron-version', `Electron v${process.versions.electron}`)
|
||||||
|
replaceText('.chrome-version', `Chromium v${process.versions.chrome}`)
|
||||||
|
replaceText('.node-version', `Node v${process.versions.node}`)
|
||||||
|
replaceText('.v8-version', `v8 v${process.versions.v8}`)
|
||||||
|
replaceText('.command-example', `${electronPath} path-to-app`)
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', initialize)
|
|
@ -1,67 +0,0 @@
|
||||||
import { remote, shell } from 'electron'
|
|
||||||
import * as fs from 'fs'
|
|
||||||
import * as path from 'path'
|
|
||||||
import * as URL from 'url'
|
|
||||||
|
|
||||||
function initialize () {
|
|
||||||
// Find the shortest path to the electron binary
|
|
||||||
const absoluteElectronPath = remote.process.execPath
|
|
||||||
const relativeElectronPath = path.relative(process.cwd(), absoluteElectronPath)
|
|
||||||
const electronPath = absoluteElectronPath.length < relativeElectronPath.length
|
|
||||||
? absoluteElectronPath
|
|
||||||
: relativeElectronPath
|
|
||||||
|
|
||||||
for (const link of document.querySelectorAll<HTMLLinkElement>('a[href]')) {
|
|
||||||
// safely add `?utm_source=default_app
|
|
||||||
const parsedUrl = URL.parse(link.getAttribute('href')!, true)
|
|
||||||
parsedUrl.query = { ...parsedUrl.query, utm_source: 'default_app' }
|
|
||||||
const url = URL.format(parsedUrl)
|
|
||||||
|
|
||||||
const openLinkExternally = (e: Event) => {
|
|
||||||
e.preventDefault()
|
|
||||||
shell.openExternalSync(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
link.addEventListener('click', openLinkExternally)
|
|
||||||
link.addEventListener('auxclick', openLinkExternally)
|
|
||||||
}
|
|
||||||
|
|
||||||
document.querySelector<HTMLAnchorElement>('.electron-version')!.innerText = `Electron v${process.versions.electron}`
|
|
||||||
document.querySelector<HTMLAnchorElement>('.chrome-version')!.innerText = `Chromium v${process.versions.chrome}`
|
|
||||||
document.querySelector<HTMLAnchorElement>('.node-version')!.innerText = `Node v${process.versions.node}`
|
|
||||||
document.querySelector<HTMLAnchorElement>('.v8-version')!.innerText = `v8 v${process.versions.v8}`
|
|
||||||
document.querySelector<HTMLAnchorElement>('.command-example')!.innerText = `${electronPath} path-to-app`
|
|
||||||
|
|
||||||
function getOcticonSvg (name: string) {
|
|
||||||
const octiconPath = path.resolve(__dirname, 'octicon', `${name}.svg`)
|
|
||||||
if (fs.existsSync(octiconPath)) {
|
|
||||||
const content = fs.readFileSync(octiconPath, 'utf8')
|
|
||||||
const div = document.createElement('div')
|
|
||||||
div.innerHTML = content
|
|
||||||
return div
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadSVG (element: HTMLSpanElement) {
|
|
||||||
for (const cssClass of element.classList) {
|
|
||||||
if (cssClass.startsWith('octicon-')) {
|
|
||||||
const icon = getOcticonSvg(cssClass.substr(8))
|
|
||||||
if (icon) {
|
|
||||||
for (const elemClass of element.classList) {
|
|
||||||
icon.classList.add(elemClass)
|
|
||||||
}
|
|
||||||
element.before(icon)
|
|
||||||
element.remove()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const element of document.querySelectorAll<HTMLSpanElement>('.octicon')) {
|
|
||||||
loadSVG(element)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('load', initialize)
|
|
|
@ -94,8 +94,9 @@ filenames = {
|
||||||
|
|
||||||
default_app_ts_sources = [
|
default_app_ts_sources = [
|
||||||
"default_app/default_app.ts",
|
"default_app/default_app.ts",
|
||||||
|
"default_app/index.ts",
|
||||||
"default_app/main.ts",
|
"default_app/main.ts",
|
||||||
"default_app/renderer.ts",
|
"default_app/preload.ts",
|
||||||
]
|
]
|
||||||
|
|
||||||
default_app_static_sources = [
|
default_app_static_sources = [
|
||||||
|
|
|
@ -158,7 +158,7 @@ const errorUtils = require('@electron/internal/common/error-utils')
|
||||||
// since browserify won't try to include `electron` in the bundle, falling back
|
// since browserify won't try to include `electron` in the bundle, falling back
|
||||||
// to the `preloadRequire` function above.
|
// to the `preloadRequire` function above.
|
||||||
function runPreloadScript (preloadSrc) {
|
function runPreloadScript (preloadSrc) {
|
||||||
const preloadWrapperSrc = `(function(require, process, Buffer, global, setImmediate, clearImmediate) {
|
const preloadWrapperSrc = `(function(require, process, Buffer, global, setImmediate, clearImmediate, exports) {
|
||||||
${preloadSrc}
|
${preloadSrc}
|
||||||
})`
|
})`
|
||||||
|
|
||||||
|
@ -166,7 +166,7 @@ function runPreloadScript (preloadSrc) {
|
||||||
const preloadFn = binding.createPreloadScript(preloadWrapperSrc)
|
const preloadFn = binding.createPreloadScript(preloadWrapperSrc)
|
||||||
const { setImmediate, clearImmediate } = require('timers')
|
const { setImmediate, clearImmediate } = require('timers')
|
||||||
|
|
||||||
preloadFn(preloadRequire, preloadProcess, Buffer, global, setImmediate, clearImmediate)
|
preloadFn(preloadRequire, preloadProcess, Buffer, global, setImmediate, clearImmediate, {})
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const { preloadPath, preloadSrc, preloadError } of preloadScripts) {
|
for (const { preloadPath, preloadSrc, preloadError } of preloadScripts) {
|
||||||
|
|
Loading…
Reference in a new issue