can1357/oh-my-pi · critical · Error

Archive entry escapes extraction directory: ${archivePath}

Error message

Archive entry escapes extraction directory: ${archivePath}

What it means

extractEmbeddedClientArchive() sanitizes each archive entry path and resolves it under the extraction root; if the resolved destination does not remain inside extractRoot, the entry is a zip-slip/path-traversal attempt and the whole extraction is aborted with this error. It protects the filesystem from a maliciously crafted embedded client archive writing outside its directory.

Source

Thrown at packages/stats/src/server.ts:70

function sanitizeArchivePath(archivePath: string): string | null {
	const normalized = archivePath.replaceAll("\\", "/").replace(/^\.\//, "");
	if (!normalized || normalized === ".") return null;
	if (normalized.includes("..") || path.isAbsolute(normalized)) return null;
	return normalized;
}

async function extractEmbeddedClientArchive(archiveBytes: Buffer, outputDir: string): Promise<void> {
	const archive = new Bun.Archive(archiveBytes);
	const files = await archive.files();
	const extractRoot = path.resolve(outputDir);

	for (const [archivePath, file] of files) {
		const sanitizedPath = sanitizeArchivePath(archivePath);
		if (!sanitizedPath) continue;
		const destinationPath = path.resolve(extractRoot, sanitizedPath);
		if (!destinationPath.startsWith(extractRoot + path.sep)) {
			throw new Error(`Archive entry escapes extraction directory: ${archivePath}`);
		}
		await Bun.write(destinationPath, file);
	}
}

async function getEmbeddedClientDir(): Promise<string> {
	if (!USE_EMBEDDED_CLIENT) return STATIC_DIR;
	if (embeddedClientDirPromise) return embeddedClientDirPromise;

	if (!EMBEDDED_CLIENT_ARCHIVE) {
		throw new Error(
			"Embedded stats client bundle missing. Rebuild the omp binary or npm bundle with embedded stats assets.",
		);
	}

	embeddedClientDirPromise = (async () => {
		const bundleHash = Bun.hash(EMBEDDED_CLIENT_ARCHIVE).toString(16);
		const outputDir = path.join(EMBEDDED_CLIENT_DIR_ROOT, bundleHash);

View on GitHub (pinned to 9690622007)

Solutions

  1. Rebuild the omp binary/npm bundle from a trusted source so the embedded archive is regenerated intact
  2. Verify the archive entries: any path containing ../ or absolute prefixes indicates a corrupted/tampered bundle
  3. Ensure sanitizeArchivePath is applied before resolve and the startsWith(extractRoot + path.sep) check runs on the resolved path
  4. If a legitimate entry triggers it, fix the entry name at build time (no leading separators, no traversal) rather than weakening the check

Example fix

// before (build step producing a bad entry)
entries.set('/../../etc/passwd-content', file);
// after
entries.set('assets/index.js', file); // relative, normalized path inside the archive root
Defensive patterns

Strategy: validation

Validate before calling

for (const [archivePath] of files) {
  const dest = path.resolve(extractRoot, sanitizeArchivePath(archivePath));
  if (!dest.startsWith(extractRoot + path.sep)) {
    throw new Error(`Refusing unsafe archive entry before extraction: ${archivePath}`);
  }
}

Type guard

function isSafeArchivePath(extractRoot: string, archivePath: string): boolean {
  const dest = path.resolve(extractRoot, sanitizeArchivePath(archivePath));
  return dest.startsWith(extractRoot + path.sep);
}

Try / catch

try {
  const dir = await getEmbeddedClientDir();
} catch (err) {
  if (err.message.startsWith('Archive entry escapes')) {
    console.error('Embedded client archive is corrupted or tampered with — rebuild the omp binary from a trusted source.');
    process.exit(1); // do not serve from a partially extracted, untrusted archive
  }
  throw err;
}

Prevention

When it happens

Trigger: An embedded client archive contains an entry whose path (after sanitizeArchivePath) still escapes the extract root — absolute paths, ../ traversal sequences, or symlinks resolving outside — when getEmbeddedClientDir first extracts the bundle (binary or npm bundle startup).

Common situations: A tampered or corrupted binary where the embedded archive was replaced; a build regression producing malformed archive entry names; extracting on a platform where path.resolve/separators make an edge-case name escape; testing with a hand-built archive containing traversal entries.

Related errors


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