From ccbd646a29ef96344a78d3b8c79d7b0084980a87 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sat, 4 Apr 2026 01:16:37 +0900 Subject: [PATCH] fix(shared): validate tar hard-link targets during preflight Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/shared/archive-entry-validator.ts | 21 +++++++++++++------ src/shared/binary-downloader.ts | 6 +++--- .../tar-zip-entry-listing.ts | 4 ++-- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/shared/archive-entry-validator.ts b/src/shared/archive-entry-validator.ts index 39d117759..46319779a 100644 --- a/src/shared/archive-entry-validator.ts +++ b/src/shared/archive-entry-validator.ts @@ -2,7 +2,7 @@ import { dirname, isAbsolute, relative, resolve, sep } from "node:path" export type ArchiveEntry = { path: string - type: "file" | "directory" | "symlink" + type: "file" | "directory" | "symlink" | "hardlink" linkPath?: string } @@ -49,26 +49,35 @@ export function validateArchiveEntries(entries: ArchiveEntry[], destDir: string) for (const entry of entries) { const resolvedEntryPath = resolveContainedPath(resolvedDestDir, entry.path, "path") - if (entry.type !== "symlink") { + if (entry.type !== "symlink" && entry.type !== "hardlink") { continue } if (!entry.linkPath) { - throw new Error(`Unsafe archive entry: symlink target missing for ${entry.path}`) + throw new Error( + `Unsafe archive entry: ${entry.type === "symlink" ? "symlink" : "hard link"} target missing for ${entry.path}` + ) } const normalizedLinkPath = normalizeArchivePath(entry.linkPath) + const linkTypeLabel = entry.type === "symlink" ? "symlink target" : "hard link target" if (isArchiveAbsolutePath(normalizedLinkPath)) { - throw new Error(`Unsafe archive entry: symlink target uses an absolute path (${entry.linkPath})`) + throw new Error( + `Unsafe archive entry: ${linkTypeLabel} uses an absolute path (${entry.linkPath})` + ) } if (containsTraversalSegment(normalizedLinkPath)) { - throw new Error(`Unsafe archive entry: symlink target contains path traversal (${entry.linkPath})`) + throw new Error( + `Unsafe archive entry: ${linkTypeLabel} contains path traversal (${entry.linkPath})` + ) } const resolvedLinkPath = resolve(dirname(resolvedEntryPath), normalizedLinkPath) if (escapesDirectory(resolvedDestDir, resolvedLinkPath)) { - throw new Error(`Unsafe archive entry: symlink target escapes extraction directory (${entry.linkPath})`) + throw new Error( + `Unsafe archive entry: ${linkTypeLabel} escapes extraction directory (${entry.linkPath})` + ) } } } diff --git a/src/shared/binary-downloader.ts b/src/shared/binary-downloader.ts index f36829c77..bb6918c30 100644 --- a/src/shared/binary-downloader.ts +++ b/src/shared/binary-downloader.ts @@ -78,15 +78,15 @@ function parseTarEntry(line: string): ArchiveEntry | null { } const [, rawType, rawEntryPath] = match - if (rawType === "l") { + if (rawType === "l" || rawType === "h") { const arrowIndex = rawEntryPath.lastIndexOf(" -> ") if (arrowIndex === -1) { - return { path: rawEntryPath, type: "symlink" } + return { path: rawEntryPath, type: rawType === "l" ? "symlink" : "hardlink" } } return { path: rawEntryPath.slice(0, arrowIndex), - type: "symlink", + type: rawType === "l" ? "symlink" : "hardlink", linkPath: rawEntryPath.slice(arrowIndex + 4), } } diff --git a/src/shared/zip-entry-listing/tar-zip-entry-listing.ts b/src/shared/zip-entry-listing/tar-zip-entry-listing.ts index 8aec02c3b..05c33ac1b 100644 --- a/src/shared/zip-entry-listing/tar-zip-entry-listing.ts +++ b/src/shared/zip-entry-listing/tar-zip-entry-listing.ts @@ -11,11 +11,11 @@ function parseTarListedZipEntry(line: string): ArchiveEntry | null { } const [, rawType, rawEntryPath] = match - if (rawType === "l") { + if (rawType === "l" || rawType === "h") { const arrowIndex = rawEntryPath.lastIndexOf(" -> ") return { path: arrowIndex === -1 ? rawEntryPath : rawEntryPath.slice(0, arrowIndex), - type: "symlink", + type: rawType === "l" ? "symlink" : "hardlink", linkPath: arrowIndex === -1 ? undefined : rawEntryPath.slice(arrowIndex + 4), } }