can1357/oh-my-pi · error · ArchiveError

Archive entry escapes extraction dir: ${entry.path}

Error message

Archive entry escapes extraction dir: ${entry.path}

What it means

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.

Source

Thrown at packages/utils/src/ar/open.ts:180

 */
export async function extractArchive(
	input: ArchiveSource,
	destDir: string,
	options: OpenArchiveOptions = {},
): Promise<number> {
	const archive = await openArchive(input, options);
	const extractRoot = path.resolve(destDir);
	await fs.mkdir(extractRoot, { recursive: true });
	let count = 0;

	// Directories first so empty ones materialize, then files, then symlinks
	// (a symlink's target may be created after it in index order).
	const files: { path: string; mode?: number }[] = [];
	const links: { path: string; target: string }[] = [];
	for (const entry of archive.indexEntries()) {
		const outputPath = path.resolve(extractRoot, entry.path);
		if (outputPath !== extractRoot && !outputPath.startsWith(extractRoot + path.sep)) {
			throw new ArchiveError(`Archive entry escapes extraction dir: ${entry.path}`);
		}
		if (entry.isDirectory) {
			if (entry.storage?.type !== "link") {
				await fs.mkdir(outputPath, { recursive: true });
				count++;
			}
			continue;
		}
		if (entry.storage?.type === "link") {
			links.push({ path: entry.path, target: entry.storage.targetPath });
			continue;
		}
		files.push({ path: entry.path, mode: entry.mode });
	}

	for (const file of files) {
		const extracted = await archive.readFile(file.path);
		const outputPath = path.resolve(extractRoot, file.path);

View on GitHub (pinned to 9690622007)

Solutions

  1. Inspect the archive's entry listing (openArchive + indexEntries) and remove/sanitize entries containing '..' segments or absolute paths.
  2. Repackage the archive with relative, root-contained member paths.
  3. If the archive is trusted and the escape is intentional, extract members manually with your own path handling instead of extractArchive.
  4. Report the archive as malicious/corrupt when it comes from an untrusted source.
Defensive patterns

Strategy: try-catch

Validate before calling

const reader = await openArchive(src);
for (const e of reader.indexEntries()) {
  const resolved = path.resolve(destRoot, e.path);
  if (!resolved.startsWith(destRoot + path.sep)) throw new Error(`unsafe entry: ${e.path}`);
}

Try / catch

try {
  await extractArchive(src, destRoot);
} catch (err) {
  if (err instanceof ArchiveError && err.message.includes("escapes extraction dir")) {
    // reject the archive; clean any partial output and report to the user
    await fs.rm(destRoot, { recursive: true, force: true });
  }
  throw err;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/22832e78551edf8d. Report an issue: GitHub.