1
0
Fork 0
mirror of https://gitlab.com/futo-org/fcast.git synced 2025-06-24 21:25:23 +00:00

Fixed privilge escalation on Linux and Windows

This commit is contained in:
Michael Hollister 2024-11-17 11:54:02 -06:00
parent 3a3c14aab7
commit de796a8673
3 changed files with 44 additions and 22 deletions

View file

@ -4467,9 +4467,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.3", "version": "7.0.5",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {

View file

@ -360,12 +360,12 @@ export default class Main {
try { try {
Main.application = app; Main.application = app;
const isUpdating = Updater.isUpdating(); const isUpdating = Updater.isUpdating();
const fileLogType = (isUpdating && !Updater.updateApplied) ? 'fileSync' : 'file'; const fileLogType = isUpdating ? 'fileSync' : 'file';
log4js.configure({ log4js.configure({
appenders: { appenders: {
out: { type: 'stdout' }, 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: { categories: {
default: { appenders: ['out', 'log'], level: 'info' }, default: { appenders: ['out', 'log'], level: 'info' },

View file

@ -6,6 +6,7 @@ import * as log4js from "log4js";
import { app } from 'electron'; import { app } from 'electron';
import { Store } from './Store'; import { Store } from './Store';
import sudo from 'sudo-prompt'; import sudo from 'sudo-prompt';
const cp = require('child_process');
const extract = require('extract-zip'); const extract = require('extract-zip');
const logger = log4js.getLogger(); const logger = log4js.getLogger();
@ -52,7 +53,6 @@ export class Updater {
private static channelVersion: string = null; private static channelVersion: string = null;
public static isDownloading: boolean = false; public static isDownloading: boolean = false;
public static updateApplied: boolean = false;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
private static async fetchJSON(url: string): Promise<any> { private static async fetchJSON(url: string): Promise<any> {
@ -93,11 +93,6 @@ export class Updater {
} }
private static async applyUpdate(src: string, dst: string) { 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 { try {
fs.accessSync(dst, fs.constants.F_OK | fs.constants.R_OK | fs.constants.W_OK | fs.constants.X_OK); 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 process.noAsar = true
fs.rmSync(dst, { recursive: true, force: true }); fs.rmSync(dst, { recursive: true, force: true });
fs.cpSync(src, dst, { recursive: true, force: true }); fs.cpSync(src, dst, { recursive: true, force: true });
process.noAsar = false
} }
catch (err) { catch (err) {
if (err.code === 'EACCES') { if (err.code === 'EACCES' || err.code === 'EPERM') {
logger.info('Update requires admin privileges. Escalating...'); logger.info('Update requires admin privileges. Escalating...');
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
const shell = process.platform === 'win32' ? 'powershell' : ''; let command: string;
const command = `${shell} rm -rf ${dst}; ${shell} cp -rf ${src} ${dst}` 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) => { sudo.exec(command, { name: 'FCast Receiver' }, (error, stdout, stderr) => {
if (error) { if (error) {
@ -134,6 +134,27 @@ export class Updater {
throw err; 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() { public static restart() {
@ -142,15 +163,15 @@ export class Updater {
const binaryName = process.platform === 'win32' ? 'fcast-receiver.exe' : 'fcast-receiver'; 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); const updateBinPath = process.platform === 'darwin' ? path.join(updateInfo.tempPath, extractionDir) : path.join(updateInfo.tempPath, extractionDir, binaryName);
app.relaunch({ execPath: updateBinPath }); Updater.relaunch(updateBinPath);
app.exit(); return;
} }
public static isUpdating(): boolean { public static isUpdating(): boolean {
try { try {
const updateInfo: UpdateInfo = JSON.parse(fs.readFileSync(Updater.updateMetadataPath, 'utf8')); const updateInfo: UpdateInfo = JSON.parse(fs.readFileSync(Updater.updateMetadataPath, 'utf8'));
Updater.updateApplied = updateInfo.updateState === 'cleanup' ? true : false; // TODO: In case of error inform user
return true; return updateInfo.updateState !== 'error';
} }
catch { catch {
return false; return false;
@ -170,17 +191,17 @@ export class Updater {
try { try {
const updateInfo: UpdateInfo = JSON.parse(fs.readFileSync(Updater.updateMetadataPath, 'utf8')); 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 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) { switch (updateInfo.updateState) {
case UpdateState.Copy: { case UpdateState.Copy: {
const binaryName = process.platform === 'win32' ? 'fcast-receiver.exe' : 'fcast-receiver';
try { try {
logger.info('Updater process started...'); logger.info('Updater process started...');
const src = path.join(updateInfo.tempPath, extractionDir); const src = path.join(updateInfo.tempPath, extractionDir);
logger.info(`Copying files from update directory ${src} to install directory ${updateInfo.installPath}`); 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; updateInfo.updateState = UpdateState.Cleanup;
fs.writeFileSync(Updater.updateMetadataPath, JSON.stringify(updateInfo)); fs.writeFileSync(Updater.updateMetadataPath, JSON.stringify(updateInfo));
@ -228,6 +249,7 @@ export class Updater {
fs.writeFileSync(Updater.updateMetadataPath, JSON.stringify(updateInfo)); fs.writeFileSync(Updater.updateMetadataPath, JSON.stringify(updateInfo));
} }
Updater.relaunch(installBinPath);
return; return;
} }