{"record":{"id":"22832e78551edf8d","repo":"can1357/oh-my-pi","slug":"archive-entry-escapes-extraction-dir-entry-path","errorCode":null,"errorMessage":"Archive entry escapes extraction dir: ${entry.path}","messagePattern":"Archive entry escapes extraction dir: (.+?)","errorType":"validation","errorClass":"ArchiveError","httpStatus":null,"severity":"error","filePath":"packages/utils/src/ar/open.ts","lineNumber":180,"sourceCode":" */\nexport async function extractArchive(\n\tinput: ArchiveSource,\n\tdestDir: string,\n\toptions: OpenArchiveOptions = {},\n): Promise<number> {\n\tconst archive = await openArchive(input, options);\n\tconst extractRoot = path.resolve(destDir);\n\tawait fs.mkdir(extractRoot, { recursive: true });\n\tlet count = 0;\n\n\t// Directories first so empty ones materialize, then files, then symlinks\n\t// (a symlink's target may be created after it in index order).\n\tconst files: { path: string; mode?: number }[] = [];\n\tconst links: { path: string; target: string }[] = [];\n\tfor (const entry of archive.indexEntries()) {\n\t\tconst outputPath = path.resolve(extractRoot, entry.path);\n\t\tif (outputPath !== extractRoot && !outputPath.startsWith(extractRoot + path.sep)) {\n\t\t\tthrow new ArchiveError(`Archive entry escapes extraction dir: ${entry.path}`);\n\t\t}\n\t\tif (entry.isDirectory) {\n\t\t\tif (entry.storage?.type !== \"link\") {\n\t\t\t\tawait fs.mkdir(outputPath, { recursive: true });\n\t\t\t\tcount++;\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tif (entry.storage?.type === \"link\") {\n\t\t\tlinks.push({ path: entry.path, target: entry.storage.targetPath });\n\t\t\tcontinue;\n\t\t}\n\t\tfiles.push({ path: entry.path, mode: entry.mode });\n\t}\n\n\tfor (const file of files) {\n\t\tconst extracted = await archive.readFile(file.path);\n\t\tconst outputPath = path.resolve(extractRoot, file.path);","sourceCodeStart":162,"sourceCodeEnd":198,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/utils/src/ar/open.ts#L162-L198","documentation":"During extractArchive, a member's path, resolved against the extraction root, points outside that root (classic zip-slip / path traversal: '../', absolute paths, or drive-escaping names). The library rejects such entries before writing anything, protecting the filesystem from archive-driven overwrites outside the target directory.","triggerScenarios":"extractArchive processing an entry whose path.resolve(extractRoot, entry.path) is not the root itself and does not start with extractRoot + path.sep — e.g. an entry named '../evil' or '/etc/passwd' inside the archive. Called by downloadTool and install.","commonSituations":"Extracting untrusted archives (downloads, package installs) crafted with path-traversal entries; archives produced by tools that wrote absolute member paths; processing old or third-party archives with '..' components.","solutions":["Inspect the archive's entry listing (openArchive + indexEntries) and remove/sanitize entries containing '..' segments or absolute paths.","Repackage the archive with relative, root-contained member paths.","If the archive is trusted and the escape is intentional, extract members manually with your own path handling instead of extractArchive.","Report the archive as malicious/corrupt when it comes from an untrusted source."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"const reader = await openArchive(src);\nfor (const e of reader.indexEntries()) {\n  const resolved = path.resolve(destRoot, e.path);\n  if (!resolved.startsWith(destRoot + path.sep)) throw new Error(`unsafe entry: ${e.path}`);\n}","typeGuard":null,"tryCatchPattern":"try {\n  await extractArchive(src, destRoot);\n} catch (err) {\n  if (err instanceof ArchiveError && err.message.includes(\"escapes extraction dir\")) {\n    // reject the archive; clean any partial output and report to the user\n    await fs.rm(destRoot, { recursive: true, force: true });\n  }\n  throw err;\n}","preventionTips":["Always extract into a dedicated empty directory.","Pre-scan entry paths with openArchive before extracting untrusted archives.","Never disable or bypass the containment check for third-party archives.","Treat traversal entries as malicious and quarantine the archive."],"tags":["archive","path-traversal","zip-slip","security","extraction"],"backgroundTag":"zip-slip-path-traversal","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}