fix(security): make tar archive preflight fail-closed on unparsed entries
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user