test(shared): add archive preflight security regressions
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -68,6 +68,21 @@ describe("validateArchiveEntries", () => {
|
|||||||
expect(rejectEscapeSymlink).toThrow(/symlink target/i)
|
expect(rejectEscapeSymlink).toThrow(/symlink target/i)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("rejects hard-link targets that escape the extraction directory", () => {
|
||||||
|
//#given
|
||||||
|
const destDir = "/tmp/archive-root"
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const rejectEscapeHardLink = () =>
|
||||||
|
validateArchiveEntries(
|
||||||
|
[{ path: "bin/tool", type: "hardlink", linkPath: "../../etc/passwd" }],
|
||||||
|
destDir
|
||||||
|
)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(rejectEscapeHardLink).toThrow(/hard link target/i)
|
||||||
|
})
|
||||||
|
|
||||||
it("accepts contained files, directories, and symlinks", () => {
|
it("accepts contained files, directories, and symlinks", () => {
|
||||||
//#given
|
//#given
|
||||||
const destDir = "/tmp/archive-root"
|
const destDir = "/tmp/archive-root"
|
||||||
@@ -120,6 +135,39 @@ describe("archive extraction preflight", () => {
|
|||||||
expect(errorMessage).toMatch(/path traversal/i)
|
expect(errorMessage).toMatch(/path traversal/i)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("rejects tar archives with hard-link traversal before extraction", async () => {
|
||||||
|
//#given
|
||||||
|
const rootDir = createTestDir()
|
||||||
|
const archivePath = join(rootDir, "malicious-hard-link.tar.gz")
|
||||||
|
const destDir = join(rootDir, "dest")
|
||||||
|
mkdirSync(destDir, { recursive: true })
|
||||||
|
const scriptPath = writePythonScript(
|
||||||
|
rootDir,
|
||||||
|
"make-malicious-hard-link-tar.py",
|
||||||
|
[
|
||||||
|
"import sys",
|
||||||
|
"import tarfile",
|
||||||
|
"with tarfile.open(sys.argv[1], 'w:gz') as archive:",
|
||||||
|
" info = tarfile.TarInfo('bin/tool')",
|
||||||
|
" info.type = tarfile.LNKTYPE",
|
||||||
|
" info.linkname = '../../etc/passwd'",
|
||||||
|
" archive.addfile(info)",
|
||||||
|
].join("\n")
|
||||||
|
)
|
||||||
|
runCommand(`python3 "${scriptPath}" "${archivePath}"`)
|
||||||
|
|
||||||
|
//#when
|
||||||
|
let errorMessage = ""
|
||||||
|
try {
|
||||||
|
await extractTarGz(archivePath, destDir)
|
||||||
|
} catch (error) {
|
||||||
|
errorMessage = error instanceof Error ? error.message : String(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(errorMessage).toMatch(/hard link target|path traversal/i)
|
||||||
|
})
|
||||||
|
|
||||||
it("rejects zip archives with symlink escapes before extraction", async () => {
|
it("rejects zip archives with symlink escapes before extraction", async () => {
|
||||||
//#given
|
//#given
|
||||||
const rootDir = createTestDir()
|
const rootDir = createTestDir()
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { describe, expect, it } from "bun:test"
|
||||||
|
|
||||||
|
import { validateArchiveEntries } from "../archive-entry-validator"
|
||||||
|
import { parsePowerShellZipEntryLine } from "./powershell-zip-entry-listing"
|
||||||
|
|
||||||
|
describe("parsePowerShellZipEntryLine", () => {
|
||||||
|
describe("#given a json entry line with tab characters in the file name", () => {
|
||||||
|
it("#when parsing and validating the entry #then preserves the full path for traversal checks", () => {
|
||||||
|
// given
|
||||||
|
const entryLine = JSON.stringify({
|
||||||
|
type: "file",
|
||||||
|
name: `safe.txt\t../../escape.txt`,
|
||||||
|
target: "",
|
||||||
|
})
|
||||||
|
|
||||||
|
// when
|
||||||
|
const parsedEntry = parsePowerShellZipEntryLine(entryLine)
|
||||||
|
const validateParsedEntry = () =>
|
||||||
|
validateArchiveEntries(parsedEntry ? [parsedEntry] : [], "/tmp/archive-root")
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(parsedEntry).toEqual({
|
||||||
|
path: `safe.txt\t../../escape.txt`,
|
||||||
|
type: "file",
|
||||||
|
})
|
||||||
|
expect(validateParsedEntry).toThrow(/path traversal/i)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user