{"record":{"id":"926970b27c01f33c","repo":"can1357/oh-my-pi","slug":"archive-symlink-escapes-extraction-dir-link-pat","errorCode":null,"errorMessage":"Archive symlink escapes extraction dir: ${link.path} -> ${link.target}","messagePattern":"Archive symlink escapes extraction dir: (.+?) -> (.+?)","errorType":"validation","errorClass":"ArchiveError","httpStatus":null,"severity":"error","filePath":"packages/utils/src/ar/open.ts","lineNumber":213,"sourceCode":"\n\tfor (const file of files) {\n\t\tconst extracted = await archive.readFile(file.path);\n\t\tconst outputPath = path.resolve(extractRoot, file.path);\n\t\tawait Bun.write(outputPath, extracted.bytes);\n\t\tconst permissions = (file.mode ?? 0) & 0o777;\n\t\tif (permissions) await fs.chmod(outputPath, permissions);\n\t\tcount++;\n\t}\n\n\tfor (const link of links) {\n\t\tconst outputPath = path.resolve(extractRoot, link.path);\n\t\t// Reader link targets are archive-root-relative (raw targets survive\n\t\t// only for links that escape the root, which cannot be materialized).\n\t\t// Rewrite to a target relative to the link's own directory so the\n\t\t// symlink resolves correctly on disk.\n\t\tconst normalizedTarget = normalizeArchiveLookupPath(link.target);\n\t\tif (normalizedTarget === undefined) {\n\t\t\tthrow new ArchiveError(`Archive symlink escapes extraction dir: ${link.path} -> ${link.target}`);\n\t\t}\n\t\tconst resolvedTarget = path.resolve(extractRoot, normalizedTarget);\n\t\tif (resolvedTarget !== extractRoot && !resolvedTarget.startsWith(extractRoot + path.sep)) {\n\t\t\tthrow new ArchiveError(`Archive symlink escapes extraction dir: ${link.path} -> ${link.target}`);\n\t\t}\n\t\tawait fs.mkdir(path.dirname(outputPath), { recursive: true });\n\t\tawait fs.symlink(path.relative(path.dirname(outputPath), resolvedTarget) || \".\", outputPath);\n\t\tcount++;\n\t}\n\n\treturn count;\n}\n","sourceCodeStart":195,"sourceCodeEnd":226,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/utils/src/ar/open.ts#L195-L226","documentation":"extractArchive encountered a symlink entry whose raw target could not be normalized to an archive-root-relative path (normalizeArchiveLookupPath returned undefined), meaning the target escapes or is malformed beyond the library's containment rules. The library refuses to materialize symlinks that could resolve outside the extraction root.","triggerScenarios":"extractArchive iterating link entries where normalizeArchiveLookupPath(link.target) is undefined — e.g. a target with a scheme/absolute form or '..'-escaping segments the normalizer rejects — at the first check in open.ts before the resolved-target containment test.","commonSituations":"Archives (tar, cpio, etc.) containing absolute symlink targets like '/usr/lib/foo' or targets with '../' that climb above the archive root; maliciously crafted archives; archives produced on systems with different link conventions (e.g. Windows-style '|'-separated LZH links).","solutions":["List the archive's entries and rewrite offending symlink targets to archive-root-relative ones, then repack.","Skip or drop symlink entries when extracting untrusted archives (many extractors have an option to ignore links).","If the link is legitimately needed outside the root, create it manually after extraction with your own validated target.","Treat the archive as hostile if it comes from an untrusted source and reject it."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"for (const e of entries) {\n  if (e.storage?.type === \"link\" && path.isAbsolute(e.storage.targetPath)) {\n    throw new Error(`absolute symlink target in archive: ${e.path} -> ${e.storage.targetPath}`);\n  }\n}","typeGuard":null,"tryCatchPattern":"try {\n  await extractArchive(src, destRoot);\n} catch (err) {\n  if (err instanceof ArchiveError && err.message.includes(\"symlink escapes\")) {\n    // option: re-extract ignoring link entries\n  }\n  throw err;\n}","preventionTips":["Pre-scan link entries and their targets before extracting untrusted archives.","Prefer archives repacked without symlinks when distributing cross-platform.","Never let archive links point outside the extraction root; rewrite them at packaging time.","Treat absolute symlink targets as hostile in downloaded archives."],"tags":["archive","symlink","path-traversal","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"}