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.

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 b0b0339
Show file tree
Hide file tree
Showing 5 changed files with 103 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": "^3.0.2"
},
"nyc": {
"extends": "../../configs/nyc.json"
Expand Down
@@ -0,0 +1,67 @@
/********************************************************************************
* 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
********************************************************************************/

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;
});

});
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.
7 changes: 7 additions & 0 deletions yarn.lock
Expand Up @@ -10928,6 +10928,13 @@ rimraf@^3.0.0:
dependencies:
glob "^7.1.3"

rimraf@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
dependencies:
glob "^7.1.3"

rimraf@~2.4.0:
version "2.4.5"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.4.5.tgz#ee710ce5d93a8fdb856fb5ea8ff0e2d75934b2da"
Expand Down

0 comments on commit b0b0339

Please sign in to comment.