Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: increase security of default_app #17318

Merged
merged 1 commit into from
Mar 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
76 changes: 70 additions & 6 deletions default_app/default_app.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { app, BrowserWindow, BrowserWindowConstructorOptions } from 'electron'
import { app, dialog, BrowserWindow, shell, ipcMain } from 'electron'
import * as path from 'path'

let mainWindow: BrowserWindow | null = null
Expand All @@ -8,18 +8,52 @@ app.on('window-all-closed', () => {
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()

const options: BrowserWindowConstructorOptions = {
const options: Electron.BrowserWindowConstructorOptions = {
width: 900,
height: 600,
autoHideMenuBar: true,
backgroundColor: '#FFFFFF',
webPreferences: {
preload: path.resolve(__dirname, 'preload.js'),
contextIsolation: true,
preload: path.resolve(__dirname, 'renderer.js'),
webviewTag: false
sandbox: true,
enableRemoteModule: false
},
useContentSize: true,
show: false
Expand All @@ -30,9 +64,39 @@ export const load = async (appUrl: string) => {
}

mainWindow = new BrowserWindow(options)

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.focus()
}

export const loadFile = async (appPath: string) => {
mainWindow = await createWindow()
mainWindow.loadFile(appPath)
mainWindow.focus()
}
13 changes: 7 additions & 6 deletions default_app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

<head>
<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="./octicon/build.css" type="text/css" rel="stylesheet" />
<script defer src="./index.js"></script>
</head>

<body>
Expand Down Expand Up @@ -52,31 +53,31 @@

<nav>
<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>
<h4>Blog</h4>
</a>
</div>
<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>
<h4>Repository</h4>
</a>
</div>
<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>
<h4>Docs</h4>
</a>
</div>
<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>
<h4>API Demos</h4>
</a>
</div>
<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>
<h4>Forge</h4>
</a>
Expand Down
30 changes: 30 additions & 0 deletions default_app/index.ts
Original file line number Diff line number Diff line change
@@ -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)
}
26 changes: 11 additions & 15 deletions default_app/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,14 @@ function showErrorMessage (message: string) {
process.exit(1)
}

async function loadApplicationByUrl (appUrl: string) {
const { load } = await import('./default_app')
load(appUrl)
async function loadApplicationByURL (appUrl: string) {
const { loadURL } = await import('./default_app')
loadURL(appUrl)
}

async function loadApplicationByFile (appPath: string) {
const { loadFile } = await import('./default_app')
loadFile(appPath)
}

function startRepl () {
Expand All @@ -156,13 +161,9 @@ if (option.file && !option.webdriver) {
const protocol = url.parse(file).protocol
const extension = path.extname(file)
if (protocol === 'http:' || protocol === 'https:' || protocol === 'file:' || protocol === 'chrome:') {
loadApplicationByUrl(file)
loadApplicationByURL(file)
} else if (extension === '.html' || extension === '.htm') {
loadApplicationByUrl(url.format({
protocol: 'file:',
slashes: true,
pathname: path.resolve(file)
}))
loadApplicationByFile(path.resolve(file))
} else {
loadApplicationPackage(file)
}
Expand Down Expand Up @@ -196,10 +197,5 @@ Options:
console.log(welcomeMessage)
}

const indexPath = path.join(__dirname, '/index.html')
loadApplicationByUrl(url.format({
protocol: 'file:',
slashes: true,
pathname: indexPath
}))
loadApplicationByFile('index.html')
}
20 changes: 20 additions & 0 deletions default_app/preload.ts
Original file line number Diff line number Diff line change
@@ -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)
67 changes: 0 additions & 67 deletions default_app/renderer.ts

This file was deleted.

3 changes: 2 additions & 1 deletion filenames.gni
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,9 @@ filenames = {

default_app_ts_sources = [
"default_app/default_app.ts",
"default_app/index.ts",
"default_app/main.ts",
"default_app/renderer.ts",
"default_app/preload.ts",
]

default_app_static_sources = [
Expand Down
4 changes: 2 additions & 2 deletions lib/sandboxed_renderer/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,15 +158,15 @@ const errorUtils = require('@electron/internal/common/error-utils')
// since browserify won't try to include `electron` in the bundle, falling back
// to the `preloadRequire` function above.
function runPreloadScript (preloadSrc) {
const preloadWrapperSrc = `(function(require, process, Buffer, global, setImmediate, clearImmediate) {
const preloadWrapperSrc = `(function(require, process, Buffer, global, setImmediate, clearImmediate, exports) {
${preloadSrc}
})`

// eval in window scope
const preloadFn = binding.createPreloadScript(preloadWrapperSrc)
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) {
Expand Down