You've already forked careful-downloader
							
							
				mirror of
				https://github.com/jakejarvis/careful-downloader.git
				synced 2025-11-04 07:40:10 -05: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/
 | 
			
		||||
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;
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user