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 11, 2020
1 parent 07a2616 commit 94fbe5b
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 3 deletions.
4 changes: 3 additions & 1 deletion packages/plugin-ext/package.json
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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<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(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;
});

});
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -24,8 +26,30 @@ export class PluginDeployerFileHandlerContextImpl implements PluginDeployerFileH
}

async unzip(sourcePath: string, destPath: string): Promise<void> {
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 {
Expand Down
Binary file not shown.

0 comments on commit 94fbe5b

Please sign in to comment.