mirror of
https://github.com/aljazceru/goose.git
synced 2026-02-11 01:24:24 +01:00
Change updater to use platform agnostic and secure zip library (#2913)
This commit is contained in:
33
ui/desktop/package-lock.json
generated
33
ui/desktop/package-lock.json
generated
@@ -27,6 +27,7 @@
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@types/yauzl": "^2.10.3",
|
||||
"ai": "^3.4.33",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -53,7 +54,8 @@
|
||||
"tailwind-merge": "^2.5.4",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"uuid": "^11.1.0"
|
||||
"uuid": "^11.1.0",
|
||||
"yauzl": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-forge/cli": "^7.5.0",
|
||||
@@ -4314,7 +4316,6 @@
|
||||
"version": "22.15.28",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.28.tgz",
|
||||
"integrity": "sha512-I0okKVDmyKR281I0UIFV7EWAWRnR0gkuSKob5wVcByyyhr7Px/slhkQapcYX4u00ekzNWaS1gznKZnuzxwo4pw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
@@ -4433,9 +4434,7 @@
|
||||
"version": "2.10.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
|
||||
"integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
@@ -5551,7 +5550,6 @@
|
||||
"version": "0.2.13",
|
||||
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
|
||||
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
@@ -8219,6 +8217,17 @@
|
||||
"@types/yauzl": "^2.9.1"
|
||||
}
|
||||
},
|
||||
"node_modules/extract-zip/node_modules/yauzl": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
|
||||
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-crc32": "~0.2.3",
|
||||
"fd-slicer": "~1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
@@ -13002,7 +13011,6 @@
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
||||
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/perfect-debounce": {
|
||||
@@ -16232,7 +16240,6 @@
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unified": {
|
||||
@@ -17259,14 +17266,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/yauzl": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
|
||||
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
|
||||
"dev": true,
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.2.0.tgz",
|
||||
"integrity": "sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-crc32": "~0.2.3",
|
||||
"fd-slicer": "~1.1.0"
|
||||
"pend": "~1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
|
||||
@@ -101,6 +101,7 @@
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@types/yauzl": "^2.10.3",
|
||||
"ai": "^3.4.33",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -127,6 +128,7 @@
|
||||
"tailwind-merge": "^2.5.4",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"uuid": "^11.1.0"
|
||||
"uuid": "^11.1.0",
|
||||
"yauzl": "^3.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { app } from 'electron';
|
||||
import { compareVersions } from 'compare-versions';
|
||||
import * as fs from 'fs/promises';
|
||||
import { createWriteStream } from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { spawn } from 'child_process';
|
||||
import * as yauzl from 'yauzl';
|
||||
import log from './logger';
|
||||
|
||||
interface GitHubRelease {
|
||||
@@ -190,40 +191,17 @@ export class GitHubUpdater {
|
||||
|
||||
log.info(`GitHubUpdater: Update downloaded to ${downloadPath}`);
|
||||
|
||||
// Auto-unzip the downloaded file
|
||||
// Auto-unzip the downloaded file using yauzl (secure ZIP library)
|
||||
try {
|
||||
const tempExtractDir = path.join(downloadsDir, `temp-extract-${Date.now()}`);
|
||||
|
||||
// Create temp extraction directory
|
||||
await fs.mkdir(tempExtractDir, { recursive: true });
|
||||
|
||||
// Use unzip command to extract
|
||||
log.info(`GitHubUpdater: Extracting ${fileName} to temp directory`);
|
||||
log.info(`GitHubUpdater: Extracting ${fileName} to temp directory using yauzl`);
|
||||
|
||||
const unzipProcess = spawn('unzip', ['-o', downloadPath, '-d', tempExtractDir]);
|
||||
|
||||
let stderr = '';
|
||||
unzipProcess.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
unzipProcess.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Unzip process exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
|
||||
unzipProcess.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
|
||||
if (stderr && !stderr.includes('warning')) {
|
||||
log.warn(`GitHubUpdater: Unzip stderr: ${stderr}`);
|
||||
}
|
||||
// Use yauzl to extract the ZIP file securely
|
||||
await extractZipFile(downloadPath, tempExtractDir);
|
||||
|
||||
// Check if Goose.app exists in the extracted content
|
||||
const appPath = path.join(tempExtractDir, 'Goose.app');
|
||||
@@ -287,5 +265,108 @@ export class GitHubUpdater {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Securely extract a ZIP file using yauzl with security checks
|
||||
* @param zipPath Path to the ZIP file
|
||||
* @param extractDir Directory to extract to
|
||||
*/
|
||||
async function extractZipFile(zipPath: string, extractDir: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
yauzl.open(zipPath, { lazyEntries: true }, (err, zipfile) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!zipfile) {
|
||||
reject(new Error('Failed to open ZIP file'));
|
||||
return;
|
||||
}
|
||||
|
||||
zipfile.readEntry();
|
||||
|
||||
zipfile.on('entry', async (entry: yauzl.Entry) => {
|
||||
try {
|
||||
// Security check: prevent directory traversal attacks
|
||||
if (entry.fileName.includes('..') || path.isAbsolute(entry.fileName)) {
|
||||
log.warn(`GitHubUpdater: Skipping potentially dangerous path: ${entry.fileName}`);
|
||||
zipfile.readEntry();
|
||||
return;
|
||||
}
|
||||
|
||||
const fullPath = path.join(extractDir, entry.fileName);
|
||||
|
||||
// Ensure the resolved path is still within the extraction directory
|
||||
const resolvedPath = path.resolve(fullPath);
|
||||
const resolvedExtractDir = path.resolve(extractDir);
|
||||
if (!resolvedPath.startsWith(resolvedExtractDir + path.sep)) {
|
||||
log.warn(`GitHubUpdater: Path traversal attempt detected: ${entry.fileName}`);
|
||||
zipfile.readEntry();
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle directories
|
||||
if (entry.fileName.endsWith('/')) {
|
||||
await fs.mkdir(fullPath, { recursive: true });
|
||||
zipfile.readEntry();
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle files
|
||||
zipfile.openReadStream(entry, async (err, readStream) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!readStream) {
|
||||
reject(new Error('Failed to open read stream'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Ensure parent directory exists
|
||||
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
||||
|
||||
// Create write stream
|
||||
const writeStream = createWriteStream(fullPath);
|
||||
|
||||
readStream.on('end', () => {
|
||||
writeStream.end();
|
||||
zipfile.readEntry();
|
||||
});
|
||||
|
||||
readStream.on('error', (streamErr) => {
|
||||
writeStream.destroy();
|
||||
reject(streamErr);
|
||||
});
|
||||
|
||||
writeStream.on('error', (writeErr: Error) => {
|
||||
reject(writeErr);
|
||||
});
|
||||
|
||||
// Pipe the data
|
||||
readStream.pipe(writeStream);
|
||||
} catch (fileErr) {
|
||||
reject(fileErr);
|
||||
}
|
||||
});
|
||||
} catch (entryErr) {
|
||||
reject(entryErr);
|
||||
}
|
||||
});
|
||||
|
||||
zipfile.on('end', () => {
|
||||
log.info('GitHubUpdater: ZIP extraction completed successfully');
|
||||
resolve();
|
||||
});
|
||||
|
||||
zipfile.on('error', (zipErr) => {
|
||||
reject(zipErr);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
export const githubUpdater = new GitHubUpdater();
|
||||
|
||||
Reference in New Issue
Block a user