Browse Source

chore: increase security of default_app (#17318)

Milan Burda 6 years ago
parent
commit
a8698d092b

+ 70 - 6
default_app/default_app.ts

@@ -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
@@ -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
@@ -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()
+}

+ 7 - 6
default_app/index.html

@@ -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>
@@ -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>

+ 30 - 0
default_app/index.ts

@@ -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)
+}

+ 11 - 15
default_app/main.ts

@@ -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 () {
@@ -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)
   }
@@ -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 - 0
default_app/preload.ts

@@ -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)

+ 0 - 67
default_app/renderer.ts

@@ -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)

+ 2 - 1
filenames.gni

@@ -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 = [

+ 2 - 2
lib/sandboxed_renderer/init.js

@@ -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
 // 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}
   })`
 
@@ -166,7 +166,7 @@ function runPreloadScript (preloadSrc) {
   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) {