mirror of
https://github.com/jakejarvis/careful-downloader.git
synced 2025-04-26 07:45:23 -04:00
Stricter paths to prevent traversal & remove custom tempDir option
This commit is contained in:
parent
211ccd401d
commit
8e89d84eda
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +1,2 @@
|
||||
node_modules/
|
||||
test/temp/
|
||||
|
@ -72,19 +72,12 @@ Default: `false`
|
||||
|
||||
Use [`decompress`](https://github.com/kevva/decompress) to extract the final download to the destination directory (assuming it's a `.zip`, `.tar`, `.tar.gz`, etc.).
|
||||
|
||||
##### tempDir
|
||||
|
||||
Type: `string`\
|
||||
Default: [`tempy.directory()`](https://github.com/sindresorhus/tempy#tempydirectoryoptions)
|
||||
|
||||
Path to temporary directory for unverified and/or unextracted downloads. Automatically generated if not set (recommended). If set manually, the directory isn't purged upon finishing for security reasons.
|
||||
|
||||
##### destDir
|
||||
|
||||
Type: `string`\
|
||||
Default: `"./downloads"`
|
||||
|
||||
Full path or directory name relative to module to store the validated download.
|
||||
Directory path relative to module where the validated download is saved or extracted. **Must be located within `process.cwd()` for security reasons.**
|
||||
|
||||
##### cleanDestDir
|
||||
|
||||
|
12
index.d.ts
vendored
12
index.d.ts
vendored
@ -16,16 +16,8 @@ export interface Options {
|
||||
readonly extract?: boolean;
|
||||
|
||||
/**
|
||||
* Path to temporary directory for unverified and/or unextracted downloads.
|
||||
* Automatically generated if not set (recommended).
|
||||
*
|
||||
* @default `tempy.directory()`
|
||||
*/
|
||||
readonly tempDir?: string;
|
||||
|
||||
/**
|
||||
* Full path or directory name relative to module to store the validated
|
||||
* download.
|
||||
* Directory path relative to module where the validated download is saved or
|
||||
* extracted. Must be located within `process.cwd()` for security reasons.
|
||||
*
|
||||
* @default "./downloads"
|
||||
*/
|
||||
|
35
index.js
35
index.js
@ -12,34 +12,35 @@ export default async function downloader(downloadUrl, checksumUrl, options) {
|
||||
// intialize options if none are set
|
||||
options = options || {};
|
||||
|
||||
// don't delete the temp dir if set manually and dir exists
|
||||
let deleteTempDir = true;
|
||||
if (options.tempDir && fs.pathExistsSync(options.tempDir)) {
|
||||
deleteTempDir = false;
|
||||
}
|
||||
|
||||
// normalize options and set defaults
|
||||
options = {
|
||||
filename: options.filename || urlParse(downloadUrl).pathname.split("/").pop(),
|
||||
extract: !!options.extract,
|
||||
tempDir: options.tempDir ? path.resolve(process.cwd(), options.tempDir) : tempy.directory(),
|
||||
destDir: options.destDir ? path.resolve(process.cwd(), options.destDir) : path.resolve(process.cwd(), "download"),
|
||||
destDir: options.destDir ? path.resolve(process.cwd(), options.destDir) : path.resolve(process.cwd(), "downloads"),
|
||||
cleanDestDir: !!options.cleanDestDir,
|
||||
algorithm: options.algorithm || "sha256",
|
||||
encoding: options.encoding || "binary",
|
||||
};
|
||||
|
||||
// throw an error if destDir is outside of the module to prevent path traversal for security reasons
|
||||
if (!options.destDir.startsWith(process.cwd())) {
|
||||
throw new Error(`destDir must be located within '${process.cwd()}', it's currently set to '${options.destDir}'.`);
|
||||
}
|
||||
|
||||
// initialize temporary directory
|
||||
const tempDir = tempy.directory();
|
||||
|
||||
try {
|
||||
// simultaneously download the desired file and its checksums
|
||||
await Promise.all([
|
||||
downloadFile(downloadUrl, path.join(options.tempDir, options.filename)),
|
||||
downloadFile(checksumUrl, path.join(options.tempDir, "checksums.txt")),
|
||||
downloadFile(downloadUrl, path.join(tempDir, options.filename)),
|
||||
downloadFile(checksumUrl, path.join(tempDir, "checksums.txt")),
|
||||
]);
|
||||
|
||||
// validate the checksum of the download
|
||||
if (await checkChecksum(options.tempDir, options.filename, "checksums.txt", options.algorithm, options.encoding)) {
|
||||
if (await checkChecksum(tempDir, options.filename, "checksums.txt", options.algorithm, options.encoding)) {
|
||||
// optionally clear the target directory of existing files
|
||||
if (options.cleanDestDir) {
|
||||
if (options.cleanDestDir && fs.existsSync(options.destDir)) {
|
||||
await fs.remove(options.destDir);
|
||||
}
|
||||
|
||||
@ -48,21 +49,19 @@ export default async function downloader(downloadUrl, checksumUrl, options) {
|
||||
|
||||
if (options.extract) {
|
||||
// decompress download and move resulting files to final destination
|
||||
await decompress(path.join(options.tempDir, options.filename), options.destDir);
|
||||
await decompress(path.join(tempDir, options.filename), options.destDir);
|
||||
return options.destDir;
|
||||
} else {
|
||||
// move verified download to final destination as-is
|
||||
await fs.copy(path.join(options.tempDir, options.filename), path.join(options.destDir, options.filename));
|
||||
await fs.copy(path.join(tempDir, options.filename), path.join(options.destDir, options.filename));
|
||||
return path.join(options.destDir, options.filename);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Invalid checksum for ${options.filename}.`);
|
||||
}
|
||||
} finally {
|
||||
// delete temporary directory (except for edge cases above)
|
||||
if (deleteTempDir) {
|
||||
await fs.remove(options.tempDir);
|
||||
}
|
||||
// delete temporary directory
|
||||
await fs.remove(tempDir);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,50 +1,63 @@
|
||||
/* eslint-env mocha */
|
||||
import fs from "fs-extra";
|
||||
import path from "path";
|
||||
import tempy from "tempy";
|
||||
import { fileURLToPath } from "url";
|
||||
import { expect } from "chai";
|
||||
|
||||
import downloader from "../index.js";
|
||||
|
||||
// https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c#what-do-i-use-instead-of-__dirname-and-__filename
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
it("verified checksum, hugo.exe was extracted", async function () {
|
||||
this.timeout(30000); // increase timeout to an excessive 30 seconds for CI
|
||||
|
||||
const outDir = path.join(tempy.directory());
|
||||
|
||||
await downloader(
|
||||
"https://github.com/gohugoio/hugo/releases/download/v0.88.1/hugo_extended_0.88.1_Windows-64bit.zip",
|
||||
"https://github.com/gohugoio/hugo/releases/download/v0.88.1/hugo_0.88.1_checksums.txt",
|
||||
{
|
||||
destDir: outDir,
|
||||
destDir: path.join(__dirname, "temp"),
|
||||
algorithm: "sha256",
|
||||
encoding: "binary",
|
||||
extract: true,
|
||||
},
|
||||
);
|
||||
|
||||
expect(fs.existsSync(path.join(outDir, "hugo.exe"))).to.be.true;
|
||||
expect(fs.existsSync(path.join(__dirname, "temp", "hugo.exe"))).to.be.true;
|
||||
|
||||
fs.removeSync(outDir);
|
||||
// clean up
|
||||
fs.removeSync(path.join(__dirname, "temp"));
|
||||
});
|
||||
|
||||
it("incorrect checksum, not extracted", async function () {
|
||||
this.timeout(30000); // increase timeout to an excessive 30 seconds for CI
|
||||
|
||||
const outDir = path.join(tempy.directory());
|
||||
|
||||
expect(async () => downloader(
|
||||
// download mismatching versions to trigger error
|
||||
"https://github.com/gohugoio/hugo/releases/download/v0.88.0/hugo_0.88.0_Windows-64bit.zip",
|
||||
"https://github.com/gohugoio/hugo/releases/download/v0.88.1/hugo_0.88.1_checksums.txt",
|
||||
{
|
||||
destDir: outDir,
|
||||
destDir: path.join(__dirname, "temp"),
|
||||
algorithm: "sha256",
|
||||
encoding: "binary",
|
||||
extract: false,
|
||||
},
|
||||
)).to.throw;
|
||||
|
||||
expect(fs.existsSync(path.join(outDir, "hugo.exe"))).to.be.false;
|
||||
expect(fs.existsSync(path.join(__dirname, "temp", "hugo.exe"))).to.be.false;
|
||||
|
||||
fs.removeSync(outDir);
|
||||
// clean up
|
||||
fs.removeSync(path.join(__dirname, "temp"));
|
||||
});
|
||||
|
||||
it("destDir located outside of module, throw error", async function () {
|
||||
this.timeout(30000); // increase timeout to an excessive 30 seconds for CI
|
||||
|
||||
expect(async () => downloader(
|
||||
"https://github.com/gohugoio/hugo/releases/download/v0.88.1/hugo_0.88.1_Windows-64bit.zip",
|
||||
"https://github.com/gohugoio/hugo/releases/download/v0.88.1/hugo_0.88.1_checksums.txt",
|
||||
{
|
||||
destDir: "../vendor", // invalid path
|
||||
},
|
||||
)).to.throw;
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user