codeburn/tests/plugin-tar-security.test.ts
iamtoruk a0adce6d5b fix(plugins): reject non-regular tar entries before extraction; cap exporter per-call surplus
Independent adversarial review found the remote-install tar validation was blind to symlinks - tar -tzf hides the link target, so a symlink (or the classic symlink-then-write-through pair) could reach the extraction step before verifyPlugin's post-extract symlink check, escaping tempDir on any tar that follows links mid-archive (macOS bsdtar refuses, GNU tar historically does not). Validation now lists with -tvzf and rejects any entry whose type flag is not a plain file or directory, platform-independently. Also bounds exporter per-call iteration to the batch size as defense beyond the existing 8MB stdout cap.
2026-08-28 06:42:34 -07:00

44 lines
1.8 KiB
TypeScript

// Independent review finding: tar entry validation was blind to symlinks
// (tar -tzf hides "-> target"), so a symlink could reach extraction before
// the post-extract symlink check runs. Reject non-regular entries up front.
import { describe, it, expect } from 'vitest'
import { mkdtemp, rm, mkdir, writeFile, symlink } from 'fs/promises'
import { join } from 'path'
import { tmpdir } from 'os'
import { execFile } from 'child_process'
import { promisify } from 'util'
import { validateTarEntries } from '../src/plugins/cli.js'
const run = promisify(execFile)
describe('tarball entry-type validation', () => {
it('rejects a tarball containing a symlink entry', async () => {
const dir = await mkdtemp(join(tmpdir(), 'tarsec-'))
try {
const build = join(dir, 'build')
await mkdir(build, { recursive: true })
await writeFile(join(build, 'ok.txt'), 'fine')
await symlink('/etc/hosts', join(build, 'evil-link'))
const tarFile = join(dir, 'evil.tgz')
await run('tar', ['-czf', tarFile, '-C', build, 'ok.txt', 'evil-link'])
await expect(validateTarEntries(tarFile)).rejects.toThrow(/non-regular entry/)
} finally {
await rm(dir, { recursive: true, force: true })
}
})
it('accepts a tarball of plain files and directories', async () => {
const dir = await mkdtemp(join(tmpdir(), 'tarsec2-'))
try {
const build = join(dir, 'build')
await mkdir(join(build, 'sub'), { recursive: true })
await writeFile(join(build, 'a.txt'), 'a')
await writeFile(join(build, 'sub', 'b.txt'), 'b')
const tarFile = join(dir, 'good.tgz')
await run('tar', ['-czf', tarFile, '-C', build, '.'])
await expect(validateTarEntries(tarFile)).resolves.toBeUndefined()
} finally {
await rm(dir, { recursive: true, force: true })
}
})
})