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/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/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); 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()', () => {