fix(shared): validate tar hard-link targets during preflight
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -2,7 +2,7 @@ import { dirname, isAbsolute, relative, resolve, sep } from "node:path"
|
|||||||
|
|
||||||
export type ArchiveEntry = {
|
export type ArchiveEntry = {
|
||||||
path: string
|
path: string
|
||||||
type: "file" | "directory" | "symlink"
|
type: "file" | "directory" | "symlink" | "hardlink"
|
||||||
linkPath?: string
|
linkPath?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,26 +49,35 @@ export function validateArchiveEntries(entries: ArchiveEntry[], destDir: string)
|
|||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const resolvedEntryPath = resolveContainedPath(resolvedDestDir, entry.path, "path")
|
const resolvedEntryPath = resolveContainedPath(resolvedDestDir, entry.path, "path")
|
||||||
if (entry.type !== "symlink") {
|
if (entry.type !== "symlink" && entry.type !== "hardlink") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!entry.linkPath) {
|
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 normalizedLinkPath = normalizeArchivePath(entry.linkPath)
|
||||||
|
const linkTypeLabel = entry.type === "symlink" ? "symlink target" : "hard link target"
|
||||||
if (isArchiveAbsolutePath(normalizedLinkPath)) {
|
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)) {
|
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)
|
const resolvedLinkPath = resolve(dirname(resolvedEntryPath), normalizedLinkPath)
|
||||||
if (escapesDirectory(resolvedDestDir, resolvedLinkPath)) {
|
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})`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,15 +78,15 @@ function parseTarEntry(line: string): ArchiveEntry | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [, rawType, rawEntryPath] = match
|
const [, rawType, rawEntryPath] = match
|
||||||
if (rawType === "l") {
|
if (rawType === "l" || rawType === "h") {
|
||||||
const arrowIndex = rawEntryPath.lastIndexOf(" -> ")
|
const arrowIndex = rawEntryPath.lastIndexOf(" -> ")
|
||||||
if (arrowIndex === -1) {
|
if (arrowIndex === -1) {
|
||||||
return { path: rawEntryPath, type: "symlink" }
|
return { path: rawEntryPath, type: rawType === "l" ? "symlink" : "hardlink" }
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
path: rawEntryPath.slice(0, arrowIndex),
|
path: rawEntryPath.slice(0, arrowIndex),
|
||||||
type: "symlink",
|
type: rawType === "l" ? "symlink" : "hardlink",
|
||||||
linkPath: rawEntryPath.slice(arrowIndex + 4),
|
linkPath: rawEntryPath.slice(arrowIndex + 4),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ function parseTarListedZipEntry(line: string): ArchiveEntry | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [, rawType, rawEntryPath] = match
|
const [, rawType, rawEntryPath] = match
|
||||||
if (rawType === "l") {
|
if (rawType === "l" || rawType === "h") {
|
||||||
const arrowIndex = rawEntryPath.lastIndexOf(" -> ")
|
const arrowIndex = rawEntryPath.lastIndexOf(" -> ")
|
||||||
return {
|
return {
|
||||||
path: arrowIndex === -1 ? rawEntryPath : rawEntryPath.slice(0, arrowIndex),
|
path: arrowIndex === -1 ? rawEntryPath : rawEntryPath.slice(0, arrowIndex),
|
||||||
type: "symlink",
|
type: rawType === "l" ? "symlink" : "hardlink",
|
||||||
linkPath: arrowIndex === -1 ? undefined : rawEntryPath.slice(arrowIndex + 4),
|
linkPath: arrowIndex === -1 ? undefined : rawEntryPath.slice(arrowIndex + 4),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user