zapier-platform-cli
Version:
The CLI for managing integrations in Zapier Developer Platform.
135 lines (113 loc) • 4.41 kB
JavaScript
// This is a modernized version of the decompress package.
// Instead of letting decompress import many of its plugins, such as
// decompress-unzip, decompress-tar, etc, we extracted only the unzip part,
// so we only depend on decompress-unzip.
// Original source: https://github.com/kevva/decompress/blob/84a8c104/index.js
const fsP = require('node:fs/promises');
const path = require('node:path');
const decompressUnzip = require('decompress-unzip');
const safeMakeDir = async (dir, realOutputPath) => {
let resolvedPathToCheck;
try {
resolvedPathToCheck = await fsP.realpath(dir);
} catch (error) {
const parent = path.dirname(dir);
resolvedPathToCheck = await safeMakeDir(parent, realOutputPath);
}
// Security check for zip slip vulnerability
if (!resolvedPathToCheck.startsWith(realOutputPath)) {
throw new Error('Refusing to create a directory outside the output path.');
}
await fsP.mkdir(dir, { recursive: true });
return await fsP.realpath(dir);
};
const preventWritingThroughSymlink = async (destination, realOutputPath) => {
let symlinkPointsTo;
try {
symlinkPointsTo = await fsP.readlink(destination);
} catch (error) {
// Either no file exists, or it's not a symlink. In either case, this is
// not an escape we need to worry about in this phase.
}
if (symlinkPointsTo) {
throw new Error('Refusing to write into a symlink');
}
};
const extractItem = async (item, realOutputPath) => {
const dest = path.join(realOutputPath, item.path);
const mode = item.mode & ~process.umask();
const now = new Date();
if (item.type === 'directory') {
await safeMakeDir(dest, realOutputPath);
await fsP.utimes(dest, now, item.mtime);
return item;
}
await safeMakeDir(path.dirname(dest), realOutputPath);
if (item.type === 'file') {
await preventWritingThroughSymlink(dest, realOutputPath);
}
const realDestinationDir = await fsP.realpath(path.dirname(dest));
if (!realDestinationDir.startsWith(realOutputPath)) {
throw new Error(
'Refusing to write outside output directory: ' + realDestinationDir,
);
}
if (item.type === 'symlink') {
// Security check to block symlinks pointing outside output directory,
// like evil_link -> ../../../../../etc/passwd. We don't throw an error
// here, just skip creating the symlink, because there are known zip files
// containing such symlinks (such as .editorconfig) that are not essential
// to the integration.
const absTargetPath = path.resolve(realDestinationDir, item.linkname);
const relTargetPath = path.relative(realOutputPath, absTargetPath);
if (!relTargetPath.startsWith('..')) {
// Windows will have issues with the following line, since creating a
// symlink on Windows requires Administrator privilege. But that's fine
// because we only run decompress on Windows in CI tests, which do run
// with Administrator privilege.
try {
await fsP.symlink(item.linkname, dest);
} catch (err) {
// When a package manager uses symlinks (e.g. pnpm), the zip may
// contain both file entries and a symlink entry for the same path.
// Since we extract in parallel, the file entries can create a
// directory at `dest` before the symlink is created, causing
// EEXIST.
if (err.code === 'EEXIST' && err.syscall === 'symlink') {
// Safe to ignore this error, the content is already there.
} else {
throw err;
}
}
}
} else {
await fsP.writeFile(dest, item.data, { mode });
await fsP.utimes(dest, now, item.mtime);
}
return item;
};
const decompress = async (input, output) => {
if (typeof input !== 'string') {
throw new TypeError('input must be a file path (string)');
}
if (typeof output !== 'string') {
throw new TypeError('output must be a directory path (string)');
}
let fd, buf;
try {
fd = await fsP.open(input);
buf = await fd.readFile();
} finally {
await fd?.close();
}
// Ensure output directory exists
await fsP.mkdir(output, { recursive: true });
const realOutputPath = await fsP.realpath(output);
const items = await decompressUnzip()(buf);
return await Promise.all(
items.map(async (x) => {
return await extractItem(x, realOutputPath);
}),
);
};
module.exports = decompress;