diff --git a/packages/cli-kit/src/public/node/archiver.integration.test.ts b/packages/cli-kit/src/public/node/archiver.integration.test.ts index 892cb055e9..3d54d647bc 100644 --- a/packages/cli-kit/src/public/node/archiver.integration.test.ts +++ b/packages/cli-kit/src/public/node/archiver.integration.test.ts @@ -58,6 +58,30 @@ describe('zip', () => { expect(expectedEntries.sort()).toEqual(archiveEntries.sort()) }) }) + + test('prevents zipping files outside the input directory', async () => { + await inTemporaryDirectory(async (tmpDir) => { + // Given + const zipPath = joinPath(tmpDir, 'output.zip') + const inputDirectory = joinPath(tmpDir, 'input') + const outsideFile = joinPath(tmpDir, 'outside.txt') + await mkdir(inputDirectory) + await touchFile(outsideFile) + fs.writeFileSync(outsideFile, 'outside content') + + // When + await zip({ + inputDirectory, + outputZipPath: zipPath, + matchFilePattern: '../outside.txt', + }) + + // Then + const archiveEntries = await readArchiveFiles(zipPath) + expect(archiveEntries).not.toContain('../outside.txt') + expect(archiveEntries).not.toContain('outside.txt') + }) + }) }) describe('brotliCompress', () => { diff --git a/packages/cli-kit/src/public/node/archiver.ts b/packages/cli-kit/src/public/node/archiver.ts index 35d781a2f7..c9fa3a4295 100644 --- a/packages/cli-kit/src/public/node/archiver.ts +++ b/packages/cli-kit/src/public/node/archiver.ts @@ -1,6 +1,7 @@ import {outputDebug, outputContent, outputToken} from './output.js' import {glob, removeFile} from './fs.js' -import {relativePath, joinPath, dirname} from './path.js' +import {relativePath, joinPath, dirname, isSubpath} from './path.js' +import {AbortError} from './error.js' import archiver from 'archiver' import {createWriteStream, readFileSync, writeFileSync} from 'fs' import {readFile} from 'fs/promises' @@ -35,13 +36,15 @@ interface ZipOptions { export async function zip(options: ZipOptions): Promise { const {inputDirectory, outputZipPath, matchFilePattern = '**/*'} = options outputDebug(outputContent`Zipping ${outputToken.path(inputDirectory)} into ${outputToken.path(outputZipPath)}`) - const pathsToZip = await glob(matchFilePattern, { + let pathsToZip = await glob(matchFilePattern, { cwd: inputDirectory, absolute: true, dot: true, followSymbolicLinks: false, }) + pathsToZip = pathsToZip.filter((filePath) => isSubpath(inputDirectory, filePath)) + return new Promise((resolve, reject) => { const archive = archiver('zip') @@ -96,6 +99,9 @@ function collectParentDirectories(fileRelativePath: string, accumulator: Set { + if (!isSubpath(inputDirectory, filePath)) { + throw new AbortError(outputContent`Skipped archiving of file ${outputToken.path(filePath)} outside the directory.`) + } const fileRelativePath = relativePath(inputDirectory, filePath) if (!filePath || !fileRelativePath) return @@ -169,8 +175,9 @@ export async function brotliCompress(options: BrotliOptions): Promise { followSymbolicLinks: false, }) .then(async (pathsToZip) => { + const filteredPaths = pathsToZip.filter((filePath) => isSubpath(options.inputDirectory, filePath)) // Read all files immediately to prevent ENOENT errors during race conditions - const addFilesPromises = pathsToZip.map(async (filePath) => { + const addFilesPromises = filteredPaths.map(async (filePath) => { await archiveFile(options.inputDirectory, filePath, archive) })