diff --git a/packages/plugin-ext/package.json b/packages/plugin-ext/package.json index 688aa324f796a..6e4c0645b27dc 100644 --- a/packages/plugin-ext/package.json +++ b/packages/plugin-ext/package.json @@ -81,7 +81,9 @@ "@types/escape-html": "^0.0.20", "@types/lodash.clonedeep": "^4.5.3", "@types/ps-tree": "^1.1.0", - "@types/request": "^2.0.3" + "@types/request": "^2.0.3", + "chai": "^4.2.0", + "rimraf": "^2.6.1" }, "nyc": { "extends": "../../configs/nyc.json" 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..02281fd2947be --- /dev/null +++ b/packages/plugin-ext/src/main/node/plugin-deployer-file-handler-context-impl.spec.ts @@ -0,0 +1,69 @@ +/******************************************************************************** + * 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'); + +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('should prevent zip-slip', 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(dest)); + + const zipSlipArchivePath = path.join(testDataPath, 'slip.tar.gz'); + const slippedFilePath = '/tmp/slipped.txt'; + + finalizers.push(() => rimraf.sync(slippedFilePath)); + rimraf.sync(slippedFilePath); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const pluginDeployerFileHandlerContext = new PluginDeployerFileHandlerContextImpl(undefined as any); + await pluginDeployerFileHandlerContext.unzip(zipSlipArchivePath, dest); + + 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..0ca46f0438c19 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,6 +14,8 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ +import * as path from 'path'; +import { promises as fs } from 'fs'; import { PluginDeployerEntry, PluginDeployerFileHandlerContext } from '../../common/plugin-protocol'; import * as decompress from 'decompress'; @@ -24,8 +26,30 @@ export class PluginDeployerFileHandlerContextImpl implements PluginDeployerFileH } async unzip(sourcePath: string, destPath: string): Promise { - await decompress(sourcePath, destPath); - return Promise.resolve(); + const zipSlipFiles = new Set<[string, string]>(); + const absoluteDestPath = await fs.realpath(destPath); + await decompress(sourcePath, absoluteDestPath, { + /** + * Prevent zip-slip: https://snyk.io/research/zip-slip-vulnerability + */ + filter(file: decompress.File): boolean { + 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))) { + zipSlipFiles.add([file.path, expectedFilePath]); + return false; // only skip the exploit files, maybe the rest is fine. + } else { + return true; + } + } + }); + if (zipSlipFiles.size > 0) { + console.error(`Detected a zip-slip exploit in archive: "${sourcePath}"`); + for (const [relativePath, expectedPath] of zipSlipFiles) { + console.error(` - File "${relativePath}" was going to write to: "${expectedPath}"`); + } + console.error('See: https://snyk.io/research/zip-slip-vulnerability'); + } } 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