From de796a86733eaaa5f49f17b0d2b935c0854f9920 Mon Sep 17 00:00:00 2001 From: Michael Hollister Date: Sun, 17 Nov 2024 11:54:02 -0600 Subject: [PATCH] Fixed privilge escalation on Linux and Windows --- receivers/electron/package-lock.json | 6 +-- receivers/electron/src/Main.ts | 4 +- receivers/electron/src/Updater.ts | 56 +++++++++++++++++++--------- 3 files changed, 44 insertions(+), 22 deletions(-) diff --git a/receivers/electron/package-lock.json b/receivers/electron/package-lock.json index 9fcd105..e492197 100644 --- a/receivers/electron/package-lock.json +++ b/receivers/electron/package-lock.json @@ -4467,9 +4467,9 @@ "license": "MIT" }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", "dev": true, "license": "MIT", "dependencies": { diff --git a/receivers/electron/src/Main.ts b/receivers/electron/src/Main.ts index 4d1a583..c7abead 100644 --- a/receivers/electron/src/Main.ts +++ b/receivers/electron/src/Main.ts @@ -360,12 +360,12 @@ export default class Main { try { Main.application = app; const isUpdating = Updater.isUpdating(); - const fileLogType = (isUpdating && !Updater.updateApplied) ? 'fileSync' : 'file'; + const fileLogType = isUpdating ? 'fileSync' : 'file'; log4js.configure({ appenders: { out: { type: 'stdout' }, - log: { type: fileLogType, filename: path.join(app.getPath('logs'), 'fcast-receiver.log'), flags: 'a', maxLogSize: '10M' }, + log: { type: fileLogType, filename: path.join(app.getPath('logs'), 'fcast-receiver.log'), flags: 'a', maxLogSize: '5M' }, }, categories: { default: { appenders: ['out', 'log'], level: 'info' }, diff --git a/receivers/electron/src/Updater.ts b/receivers/electron/src/Updater.ts index 49c55a8..dd87333 100644 --- a/receivers/electron/src/Updater.ts +++ b/receivers/electron/src/Updater.ts @@ -6,6 +6,7 @@ import * as log4js from "log4js"; import { app } from 'electron'; import { Store } from './Store'; import sudo from 'sudo-prompt'; +const cp = require('child_process'); const extract = require('extract-zip'); const logger = log4js.getLogger(); @@ -52,7 +53,6 @@ export class Updater { private static channelVersion: string = null; public static isDownloading: boolean = false; - public static updateApplied: boolean = false; // eslint-disable-next-line @typescript-eslint/no-explicit-any private static async fetchJSON(url: string): Promise { @@ -93,11 +93,6 @@ export class Updater { } private static async applyUpdate(src: string, dst: string) { - // Sanity removal protection check (especially under admin) - if (!dst.includes('fcast-receiver')) { - throw `Aborting update applying due to possible malformed path: ${dst}`; - } - try { fs.accessSync(dst, fs.constants.F_OK | fs.constants.R_OK | fs.constants.W_OK | fs.constants.X_OK); @@ -105,15 +100,20 @@ export class Updater { process.noAsar = true fs.rmSync(dst, { recursive: true, force: true }); fs.cpSync(src, dst, { recursive: true, force: true }); - process.noAsar = false } catch (err) { - if (err.code === 'EACCES') { + if (err.code === 'EACCES' || err.code === 'EPERM') { logger.info('Update requires admin privileges. Escalating...'); await new Promise((resolve, reject) => { - const shell = process.platform === 'win32' ? 'powershell' : ''; - const command = `${shell} rm -rf ${dst}; ${shell} cp -rf ${src} ${dst}` + let command: string; + if (process.platform === 'win32') { + // Using native cmd.exe seems to create less issues than using powershell... + command = `rmdir /S /Q "${dst}" & xcopy /Y /E "${src}" "${dst}"`; + } + else { + command = `rm -rf '${dst}'; cp -rf '${src}' '${dst}'`; + } sudo.exec(command, { name: 'FCast Receiver' }, (error, stdout, stderr) => { if (error) { @@ -134,6 +134,27 @@ export class Updater { throw err; } } + finally { + process.noAsar = false; + } + } + + // Cannot use app.relaunch(...) since it breaks privilege escalation on Linux... + private static relaunch(binPath: string) { + log4js.shutdown(); + + let proc; + if (process.platform === 'win32') { + // cwd is bugged on Windows, perhaps due to needing to be in system32 to launch cmd.exe + proc = cp.spawn(`"${binPath}"`, [], { stdio: 'ignore', shell: true, detached: true, windowsHide: true }); + } + else { + proc = cp.spawn(binPath, [], { cwd: path.dirname(binPath), stdio: 'ignore', detached: true }); + } + + proc.unref(); + app.exit(); + return; } public static restart() { @@ -142,15 +163,15 @@ export class Updater { const binaryName = process.platform === 'win32' ? 'fcast-receiver.exe' : 'fcast-receiver'; const updateBinPath = process.platform === 'darwin' ? path.join(updateInfo.tempPath, extractionDir) : path.join(updateInfo.tempPath, extractionDir, binaryName); - app.relaunch({ execPath: updateBinPath }); - app.exit(); + Updater.relaunch(updateBinPath); + return; } public static isUpdating(): boolean { try { const updateInfo: UpdateInfo = JSON.parse(fs.readFileSync(Updater.updateMetadataPath, 'utf8')); - Updater.updateApplied = updateInfo.updateState === 'cleanup' ? true : false; - return true; + // TODO: In case of error inform user + return updateInfo.updateState !== 'error'; } catch { return false; @@ -170,17 +191,17 @@ export class Updater { try { const updateInfo: UpdateInfo = JSON.parse(fs.readFileSync(Updater.updateMetadataPath, 'utf8')); const extractionDir = process.platform === 'darwin' ? 'FCast Receiver.app' : `fcast-receiver-${process.platform}-${process.arch}`; + const binaryName = process.platform === 'win32' ? 'fcast-receiver.exe' : 'fcast-receiver'; + const installBinPath = path.join(updateInfo.installPath, binaryName); switch (updateInfo.updateState) { case UpdateState.Copy: { - const binaryName = process.platform === 'win32' ? 'fcast-receiver.exe' : 'fcast-receiver'; - try { logger.info('Updater process started...'); const src = path.join(updateInfo.tempPath, extractionDir); logger.info(`Copying files from update directory ${src} to install directory ${updateInfo.installPath}`); - Updater.applyUpdate(src, updateInfo.installPath); + await Updater.applyUpdate(src, updateInfo.installPath); updateInfo.updateState = UpdateState.Cleanup; fs.writeFileSync(Updater.updateMetadataPath, JSON.stringify(updateInfo)); @@ -228,6 +249,7 @@ export class Updater { fs.writeFileSync(Updater.updateMetadataPath, JSON.stringify(updateInfo)); } + Updater.relaunch(installBinPath); return; }