diff --git a/packages/plugin-ext/src/main/node/plugin-deployer-file-handler-context-impl.spec.ts b/packages/plugin-ext/src/main/node/plugin-deployer-file-handler-context-impl.spec.ts new file mode 100644 index 0000000000000..f877949c62ebb --- /dev/null +++ b/packages/plugin-ext/src/main/node/plugin-deployer-file-handler-context-impl.spec.ts @@ -0,0 +1,87 @@ +/******************************************************************************** + * Copyright (C) 2020 Ericsson and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +/* eslint-disable no-unused-expressions */ + +import * as fs from 'fs'; +import * as path from 'path'; +import rimraf = require('rimraf'); +import { expect } from 'chai'; +import { PluginDeployerFileHandlerContextImpl } from './plugin-deployer-file-handler-context-impl'; + +const testDataPath = path.join(__dirname, '../../../src/main/node/test-data'); +const zipSlipArchivePath = path.join(testDataPath, 'slip.tar.gz'); +const slippedFilePath = '/tmp/slipped.txt'; + +describe('PluginDeployerFileHandlerContextImpl', () => { + + /** + * Clean resources after a test. + */ + const finalizers: Array<() => void> = []; + + beforeEach(() => { + finalizers.length = 0; + }); + + afterEach(() => { + for (const finalize of finalizers) { + try { + finalize(); + } catch (error) { + console.error(error); + } + } + }); + + it('zip-slip should happen if we do not prevent it', async function (): Promise { + if (process.platform === 'win32') { + this.skip(); // Test will not work on Windows (because of the /tmp path) + } + + const dest = fs.mkdtempSync('/tmp/plugin-ext-test'); + finalizers.push(() => rimraf.sync(slippedFilePath)); + finalizers.push(() => rimraf.sync(dest)); + rimraf.sync(slippedFilePath); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const pluginDeployerFileHandlerContext = new PluginDeployerFileHandlerContextImpl(undefined as any); + pluginDeployerFileHandlerContext['_safeUnzip'] = false; + const success: boolean = await pluginDeployerFileHandlerContext.unzip(zipSlipArchivePath, dest).then(() => true, () => false); + + expect(success).true; + expect(fs.existsSync(slippedFilePath)).true; + }); + + it('should prevent zip-slip by default', async function (): Promise { + if (process.platform === 'win32') { + this.skip(); // Test will not work on Windows (because of the /tmp path) + } + + const dest = fs.mkdtempSync('/tmp/plugin-ext-test'); + finalizers.push(() => rimraf.sync(slippedFilePath)); + finalizers.push(() => rimraf.sync(dest)); + rimraf.sync(slippedFilePath); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const pluginDeployerFileHandlerContext = new PluginDeployerFileHandlerContextImpl(undefined as any); + const success: boolean = await pluginDeployerFileHandlerContext.unzip(zipSlipArchivePath, dest).then(() => true, () => false); + + expect(success).false; + expect(fs.existsSync(slippedFilePath)).false; + }); + +}); diff --git a/packages/plugin-ext/src/main/node/plugin-deployer-file-handler-context-impl.ts b/packages/plugin-ext/src/main/node/plugin-deployer-file-handler-context-impl.ts index d8b469f44c676..f42c90bb24b84 100644 --- a/packages/plugin-ext/src/main/node/plugin-deployer-file-handler-context-impl.ts +++ b/packages/plugin-ext/src/main/node/plugin-deployer-file-handler-context-impl.ts @@ -14,18 +14,40 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ +import * as path from 'path'; import { PluginDeployerEntry, PluginDeployerFileHandlerContext } from '../../common/plugin-protocol'; import * as decompress from 'decompress'; export class PluginDeployerFileHandlerContextImpl implements PluginDeployerFileHandlerContext { + /** + * For testing: set to false to disable zip-slip prevention. + */ + private _safeUnzip = true; + constructor(private readonly pluginDeployerEntry: PluginDeployerEntry) { } async unzip(sourcePath: string, destPath: string): Promise { - await decompress(sourcePath, destPath); - return Promise.resolve(); + const absoluteDestPath = path.resolve(process.cwd(), destPath); + await decompress(sourcePath, absoluteDestPath, { + /** + * Prevent zip-slip: https://snyk.io/research/zip-slip-vulnerability + */ + filter: (file: decompress.File) => { + if (this._safeUnzip) { + const expectedFilePath = path.join(absoluteDestPath, file.path); + // If dest is not found in the expected path, it means file will be unpacked somewhere else. + if (!expectedFilePath.startsWith(path.join(absoluteDestPath, path.sep))) { + throw new Error(`Detected a zip-slip exploit in archive "${sourcePath}"\n` + + ` File "${file.path}" was going to write to "${expectedFilePath}"\n` + + ' See: https://snyk.io/research/zip-slip-vulnerability'); + } + } + return true; + } + }); } pluginEntry(): PluginDeployerEntry { diff --git a/packages/plugin-ext/src/main/node/test-data/slip.tar.gz b/packages/plugin-ext/src/main/node/test-data/slip.tar.gz new file mode 100644 index 0000000000000..68369b16bb4fe Binary files /dev/null and b/packages/plugin-ext/src/main/node/test-data/slip.tar.gz differ