Skip to content

Commit

Permalink
plugin-ext: validate path when unpacking archives
Browse files Browse the repository at this point in the history
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
kevva/decompress#71 (comment)

Fixes #7319

Signed-off-by: Paul Maréchal <paul.marechal@ericsson.com>
  • Loading branch information
paul-marechal committed Mar 12, 2020
1 parent 93ffbab commit 2f50182
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 3 deletions.
4 changes: 3 additions & 1 deletion packages/plugin-ext/package.json
Expand Up @@ -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"
Expand Down
@@ -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<void> {
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<void> {
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;
});

});
Expand Up @@ -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<void> {
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 {
Expand Down
Binary file not shown.

0 comments on commit 2f50182

Please sign in to comment.