From 43c5e3363d1f6eba9bf42e2f18268a1fda67bbe5 Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Sat, 4 Apr 2026 16:47:32 -0700 Subject: [PATCH 1/2] fix: reject fromEntries filenames that resolve outside the fiddle dir --- src/fiddle.ts | 23 +++++++++++++++-------- tests/fiddle.test.ts | 11 +++++++++++ 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/fiddle.ts b/src/fiddle.ts index 44ec179..ae36459 100644 --- a/src/fiddle.ts +++ b/src/fiddle.ts @@ -92,19 +92,26 @@ export class FiddleFactory { const md5sum = createHash('md5'); for (const content of map.values()) md5sum.update(content); const hash = md5sum.digest('hex'); - const folder = path.join(this.fiddles, hash); + const folder = path.resolve(this.fiddles, hash); await fs.promises.mkdir(folder, { recursive: true }); d({ folder }); // save content to that temp directory await Promise.all( - [...map.entries()].map(([filename, content]) => - util.promisify(fs.writeFile)( - path.join(folder, filename), - content, - 'utf8', - ), - ), + [...map.entries()].map(([filename, content]) => { + const filePath = path.resolve(folder, filename); + const relative = path.relative(folder, filePath); + if ( + !relative || + relative.startsWith('..') || + path.isAbsolute(relative) + ) { + throw new Error( + `Refusing to write file outside of fiddle: "${filename}"`, + ); + } + return util.promisify(fs.writeFile)(filePath, content, 'utf8'); + }), ); return new Fiddle(path.join(folder, 'main.js'), 'entries'); diff --git a/tests/fiddle.test.ts b/tests/fiddle.test.ts index 27b4375..df6db24 100644 --- a/tests/fiddle.test.ts +++ b/tests/fiddle.test.ts @@ -84,6 +84,17 @@ describe('FiddleFactory', () => { } }); + it('rejects entries with filenames that escape the fiddle directory', async () => { + const files: [string, string][] = [ + ['main.js', '"use strict";'], + [path.join('..', '..', 'escaped.txt'), 'pwned'], + ]; + await expect(fiddleFactory.create(files)).rejects.toThrow( + /outside of fiddle/, + ); + expect(fs.existsSync(path.join(tmpdir, 'escaped.txt'))).toBe(false); + }); + it('reads fiddles from gists', async () => { const gistId = '642fa8daaebea6044c9079e3f8a46390'; const fiddle = await fiddleFactory.create(gistId); From c53362b055df99221c7617d0c64957bfb1d91a79 Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Sat, 4 Apr 2026 16:49:30 -0700 Subject: [PATCH 2/2] fix: validate version argument in Installer methods --- src/installer.ts | 8 ++++++++ tests/installer.test.ts | 15 +++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/installer.ts b/src/installer.ts index ba9cef0..330e27d 100644 --- a/src/installer.ts +++ b/src/installer.ts @@ -14,6 +14,12 @@ function getZipName(version: string): string { return `electron-v${version}-${process.platform}-${process.arch}.zip`; } +function assertValidVersion(version: string): void { + if (!semver.valid(version)) { + throw new Error(`Invalid Electron version: "${version}"`); + } +} + export type ProgressObject = { percent: number }; /** @@ -163,6 +169,7 @@ export class Installer extends EventEmitter { public async remove(version: string): Promise { const d = debug('fiddle-core:Installer:remove'); d(version); + assertValidVersion(version); let isBinaryDeleted = false; // utility to re-run removal functions upon failure // due to windows filesystem lockfile jank @@ -270,6 +277,7 @@ export class Installer extends EventEmitter { opts?: Partial, ): Promise { const d = debug(`fiddle-core:Installer:${version}:ensureDownloadedImpl`); + assertValidVersion(version); const { electronDownloads } = this.paths; const zipFile = path.join(electronDownloads, getZipName(version)); const zipFileExists = fs.existsSync(zipFile); diff --git a/tests/installer.test.ts b/tests/installer.test.ts index f16297b..c66300d 100644 --- a/tests/installer.test.ts +++ b/tests/installer.test.ts @@ -243,6 +243,12 @@ describe('Installer', () => { expect(nockScope.isDone()); }); + it('rejects versions that are not valid semver', async () => { + await expect( + installer.ensureDownloaded(path.join('..', 'x')), + ).rejects.toThrow(/Invalid Electron version/); + }); + it('resets install state on error', async () => { // setup: version is not installed expect(installer.state(version)).toBe(missing); @@ -297,6 +303,15 @@ describe('Installer', () => { expect(fs.existsSync(extractDir)).toBe(false); expect(events).toStrictEqual([{ version, state: missing }]); }); + + it('rejects versions that are not valid semver', async () => { + const canary = path.join(tmpdir, 'canary'); + await fs.promises.mkdir(canary); + await expect(installer.remove(path.join('..', 'canary'))).rejects.toThrow( + /Invalid Electron version/, + ); + expect(fs.existsSync(canary)).toBe(true); + }); }); describe('install()', () => {