You've already forked careful-downloader
mirror of
https://github.com/jakejarvis/careful-downloader.git
synced 2025-09-18 13:45:32 -04:00
Stricter paths to prevent traversal & remove custom tempDir option
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
node_modules/
|
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.).
|
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
|
##### destDir
|
||||||
|
|
||||||
Type: `string`\
|
Type: `string`\
|
||||||
Default: `"./downloads"`
|
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
|
##### cleanDestDir
|
||||||
|
|
||||||
|
12
index.d.ts
vendored
12
index.d.ts
vendored
@@ -16,16 +16,8 @@ export interface Options {
|
|||||||
readonly extract?: boolean;
|
readonly extract?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Path to temporary directory for unverified and/or unextracted downloads.
|
* Directory path relative to module where the validated download is saved or
|
||||||
* Automatically generated if not set (recommended).
|
* extracted. Must be located within `process.cwd()` for security reasons.
|
||||||
*
|
|
||||||
* @default `tempy.directory()`
|
|
||||||
*/
|
|
||||||
readonly tempDir?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Full path or directory name relative to module to store the validated
|
|
||||||
* download.
|
|
||||||
*
|
*
|
||||||
* @default "./downloads"
|
* @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
|
// intialize options if none are set
|
||||||
options = options || {};
|
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
|
// normalize options and set defaults
|
||||||
options = {
|
options = {
|
||||||
filename: options.filename || urlParse(downloadUrl).pathname.split("/").pop(),
|
filename: options.filename || urlParse(downloadUrl).pathname.split("/").pop(),
|
||||||
extract: !!options.extract,
|
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(), "downloads"),
|
||||||
destDir: options.destDir ? path.resolve(process.cwd(), options.destDir) : path.resolve(process.cwd(), "download"),
|
|
||||||
cleanDestDir: !!options.cleanDestDir,
|
cleanDestDir: !!options.cleanDestDir,
|
||||||
algorithm: options.algorithm || "sha256",
|
algorithm: options.algorithm || "sha256",
|
||||||
encoding: options.encoding || "binary",
|
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 {
|
try {
|
||||||
// simultaneously download the desired file and its checksums
|
// simultaneously download the desired file and its checksums
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
downloadFile(downloadUrl, path.join(options.tempDir, options.filename)),
|
downloadFile(downloadUrl, path.join(tempDir, options.filename)),
|
||||||
downloadFile(checksumUrl, path.join(options.tempDir, "checksums.txt")),
|
downloadFile(checksumUrl, path.join(tempDir, "checksums.txt")),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// validate the checksum of the download
|
// 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
|
// optionally clear the target directory of existing files
|
||||||
if (options.cleanDestDir) {
|
if (options.cleanDestDir && fs.existsSync(options.destDir)) {
|
||||||
await fs.remove(options.destDir);
|
await fs.remove(options.destDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,21 +49,19 @@ export default async function downloader(downloadUrl, checksumUrl, options) {
|
|||||||
|
|
||||||
if (options.extract) {
|
if (options.extract) {
|
||||||
// decompress download and move resulting files to final destination
|
// 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;
|
return options.destDir;
|
||||||
} else {
|
} else {
|
||||||
// move verified download to final destination as-is
|
// 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);
|
return path.join(options.destDir, options.filename);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Invalid checksum for ${options.filename}.`);
|
throw new Error(`Invalid checksum for ${options.filename}.`);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
// delete temporary directory (except for edge cases above)
|
// delete temporary directory
|
||||||
if (deleteTempDir) {
|
await fs.remove(tempDir);
|
||||||
await fs.remove(options.tempDir);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,50 +1,63 @@
|
|||||||
/* eslint-env mocha */
|
/* eslint-env mocha */
|
||||||
import fs from "fs-extra";
|
import fs from "fs-extra";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import tempy from "tempy";
|
import { fileURLToPath } from "url";
|
||||||
import { expect } from "chai";
|
import { expect } from "chai";
|
||||||
|
|
||||||
import downloader from "../index.js";
|
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 () {
|
it("verified checksum, hugo.exe was extracted", async function () {
|
||||||
this.timeout(30000); // increase timeout to an excessive 30 seconds for CI
|
this.timeout(30000); // increase timeout to an excessive 30 seconds for CI
|
||||||
|
|
||||||
const outDir = path.join(tempy.directory());
|
|
||||||
|
|
||||||
await downloader(
|
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_extended_0.88.1_Windows-64bit.zip",
|
||||||
"https://github.com/gohugoio/hugo/releases/download/v0.88.1/hugo_0.88.1_checksums.txt",
|
"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",
|
algorithm: "sha256",
|
||||||
encoding: "binary",
|
encoding: "binary",
|
||||||
extract: true,
|
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 () {
|
it("incorrect checksum, not extracted", async function () {
|
||||||
this.timeout(30000); // increase timeout to an excessive 30 seconds for CI
|
this.timeout(30000); // increase timeout to an excessive 30 seconds for CI
|
||||||
|
|
||||||
const outDir = path.join(tempy.directory());
|
|
||||||
|
|
||||||
expect(async () => downloader(
|
expect(async () => downloader(
|
||||||
// download mismatching versions to trigger error
|
// 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.0/hugo_0.88.0_Windows-64bit.zip",
|
||||||
"https://github.com/gohugoio/hugo/releases/download/v0.88.1/hugo_0.88.1_checksums.txt",
|
"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",
|
algorithm: "sha256",
|
||||||
encoding: "binary",
|
encoding: "binary",
|
||||||
extract: false,
|
extract: false,
|
||||||
},
|
},
|
||||||
)).to.throw;
|
)).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;
|
||||||
});
|
});
|
||||||
|
Reference in New Issue
Block a user