fix(security): make tar archive preflight fail-closed on unparsed entries

This commit is contained in:
YeonGyu-Kim
2026-04-04 14:27:46 +09:00
parent 2d72f51a92
commit d368f77fcd
2 changed files with 208 additions and 6 deletions
@@ -1,6 +1,10 @@
import { spawn } from "bun"
import type { ArchiveEntry } from "../archive-entry-validator"
import { log } from "../logger"
const MAX_UNPARSED_TAR_LINE_RATIO = 0.1
const MAX_UNPARSED_TAR_LINE_COUNT = 5
function parseTarListedZipEntry(line: string): ArchiveEntry | null {
const match = line.match(
@@ -26,6 +30,54 @@ function parseTarListedZipEntry(line: string): ArchiveEntry | null {
}
}
function validateParsedTarListing(
totalLineCount: number,
unparsedLines: string[]
): void {
if (unparsedLines.length === 0) {
return
}
const unparsedLineRatio = unparsedLines.length / totalLineCount
if (
unparsedLineRatio > MAX_UNPARSED_TAR_LINE_RATIO ||
unparsedLines.length > MAX_UNPARSED_TAR_LINE_COUNT
) {
throw new Error(
`zip entry listing failed: tar output format drift detected (${unparsedLines.length}/${totalLineCount} lines unparsed)`
)
}
}
export function parseTarListingOutput(stdout: string): ArchiveEntry[] {
const listingLines = stdout
.split(/\r?\n/)
.map(line => line.trim())
.filter(Boolean)
if (listingLines.length === 0) {
return []
}
const parsedEntries: ArchiveEntry[] = []
const unparsedLines: string[] = []
for (const listingLine of listingLines) {
const parsedEntry = parseTarListedZipEntry(listingLine)
if (parsedEntry === null) {
unparsedLines.push(listingLine)
log("warning: unparsed tar listing line", { line: listingLine })
continue
}
parsedEntries.push(parsedEntry)
}
validateParsedTarListing(listingLines.length, unparsedLines)
return parsedEntries
}
export async function listZipEntriesWithTar(
archivePath: string
): Promise<ArchiveEntry[]> {
@@ -44,10 +96,5 @@ export async function listZipEntriesWithTar(
throw new Error(`zip entry listing failed (exit ${exitCode}): ${stderr}`)
}
return stdout
.split(/\r?\n/)
.map(line => line.trim())
.filter(Boolean)
.map(line => parseTarListedZipEntry(line))
.filter((entry): entry is ArchiveEntry => entry !== null)
return parseTarListingOutput(stdout)
}