From a8698532e440db912b8b8aa58394523f841bc5b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20Mar=C3=A9chal?= Date: Wed, 11 Mar 2020 18:40:46 -0400 Subject: [PATCH] plugin-ext: validate path when unpacking archives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix zip-slip by validating where a given file will be unpacked. If the expected path is outside of the destination folder: log a warning and ignore the file. This commit includes an archive that will trigger the exploit by writing a file to `/tmp/slipped.txt`. This comes from magicOz, who reported the vulnerability on kevva's decompress repository (issue 71). Fixes https://github.com/eclipse-theia/theia/issues/7319 Signed-off-by: Paul Maréchal --- ...deployer-file-handler-context-impl.spec.ts | 87 ++++++++++++++++++ ...ugin-deployer-file-handler-context-impl.ts | 26 +++++- .../src/main/node/test-data/slip.tar.gz | Bin 0 -> 210 bytes 3 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 packages/plugin-ext/src/main/node/plugin-deployer-file-handler-context-impl.spec.ts create mode 100644 packages/plugin-ext/src/main/node/test-data/slip.tar.gz 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 0000000000000000000000000000000000000000..68369b16bb4fed513050fdd2a46252281c79308d GIT binary patch literal 210 zcmb2|=HQs{Xj&}O|J+L5;+)I^y^_QthPRjZavd@dX?u7k&UKxF@1-TNq9Qsa8aGb* z2Dtpu-q!Tx)lsn{N9Q|oPyPPzfaRPN&j-PcTx~BuZtyd2U;BgiZq>D|-FIvAz8#Cm zZuN0xKbN}I!t>`>_irshj9(K&e=BK}&t4Rv8(O;Zi2aL4&+{rf7O6c}`}S+&_BDcP zU(Qv_OUZNPt}Tx9xSRT~zjSZ(-W}iH?OFbFzWU5F%NNI-yQFej?QtUm5^$Iw!#H8x K^3Mz!3=9CfYh?WZ literal 0 HcmV?d00001