Files
pnpm/fetching/binary-fetcher/scripts/create-fixtures.ts

101 lines
4.5 KiB
TypeScript

/**
* Script to generate malicious ZIP fixtures for path traversal testing.
*
* AdmZip's addFile() sanitizes paths automatically, so we need to create
* raw ZIP files manually to test path traversal protection.
*
* Run with: node --experimental-strip-types scripts/create-fixtures.ts
*/
import fs from 'fs'
import path from 'path'
/**
* Create a minimal ZIP file with a given entry path (not sanitized).
* This creates a valid ZIP structure with a single uncompressed file entry.
*/
function createZipWithEntry (entryPath: string, content: string): Buffer {
const contentBuf = Buffer.from(content)
// Local file header (30 bytes + filename)
const localHeader = Buffer.alloc(30 + entryPath.length)
localHeader.writeUInt32LE(0x04034b50, 0) // Local file header signature
localHeader.writeUInt16LE(20, 4) // Version needed to extract
localHeader.writeUInt16LE(0, 6) // General purpose flags
localHeader.writeUInt16LE(0, 8) // Compression method (0 = store)
localHeader.writeUInt16LE(0, 10) // Last mod file time
localHeader.writeUInt16LE(0, 12) // Last mod file date
localHeader.writeUInt32LE(0, 14) // CRC-32 (fake but okay for tests)
localHeader.writeUInt32LE(contentBuf.length, 18) // Compressed size
localHeader.writeUInt32LE(contentBuf.length, 22) // Uncompressed size
localHeader.writeUInt16LE(entryPath.length, 26) // Filename length
localHeader.writeUInt16LE(0, 28) // Extra field length
localHeader.write(entryPath, 30, 'utf-8') // Filename
const cdOffset = localHeader.length + contentBuf.length
// Central directory header (46 bytes + filename)
const centralDir = Buffer.alloc(46 + entryPath.length)
centralDir.writeUInt32LE(0x02014b50, 0) // Central file header signature
centralDir.writeUInt16LE(20, 4) // Version made by
centralDir.writeUInt16LE(20, 6) // Version needed to extract
centralDir.writeUInt16LE(0, 8) // General purpose flags
centralDir.writeUInt16LE(0, 10) // Compression method
centralDir.writeUInt16LE(0, 12) // Last mod file time
centralDir.writeUInt16LE(0, 14) // Last mod file date
centralDir.writeUInt32LE(0, 16) // CRC-32
centralDir.writeUInt32LE(contentBuf.length, 20) // Compressed size
centralDir.writeUInt32LE(contentBuf.length, 24) // Uncompressed size
centralDir.writeUInt16LE(entryPath.length, 28) // Filename length
centralDir.writeUInt16LE(0, 30) // Extra field length
centralDir.writeUInt16LE(0, 32) // File comment length
centralDir.writeUInt16LE(0, 34) // Disk number start
centralDir.writeUInt16LE(0, 36) // Internal file attributes
centralDir.writeUInt32LE(0, 38) // External file attributes
centralDir.writeUInt32LE(0, 42) // Relative offset of local header
centralDir.write(entryPath, 46, 'utf-8')
// End of central directory record (22 bytes)
const endRecord = Buffer.alloc(22)
endRecord.writeUInt32LE(0x06054b50, 0) // End of central directory signature
endRecord.writeUInt16LE(0, 4) // Number of this disk
endRecord.writeUInt16LE(0, 6) // Disk with central directory
endRecord.writeUInt16LE(1, 8) // Entries on this disk
endRecord.writeUInt16LE(1, 10) // Total entries
endRecord.writeUInt32LE(centralDir.length, 12) // Size of central directory
endRecord.writeUInt32LE(cdOffset, 16) // Offset of central directory
endRecord.writeUInt16LE(0, 20) // ZIP file comment length
return Buffer.concat([localHeader, contentBuf, centralDir, endRecord])
}
// Ensure fixtures directory exists
const fixturesDir = path.join(import.meta.dirname, '..', 'test', 'fixtures')
fs.mkdirSync(fixturesDir, { recursive: true })
// Create path traversal ZIP (../../../ prefix)
const pathTraversalZip = createZipWithEntry(
'../../../.npmrc',
'registry=https://evil.com/\n'
)
fs.writeFileSync(path.join(fixturesDir, 'path-traversal.zip'), pathTraversalZip)
console.log('Created: test/fixtures/path-traversal.zip')
// Create absolute path ZIP (/etc/passwd)
const absolutePathZip = createZipWithEntry(
'/etc/passwd',
'root:x:0:0:root:/root:/bin/bash'
)
fs.writeFileSync(path.join(fixturesDir, 'absolute-path.zip'), absolutePathZip)
console.log('Created: test/fixtures/absolute-path.zip')
// Create Windows-style backslash path traversal ZIP
// This is only dangerous on Windows (on Unix, backslash is a valid filename char)
const backslashTraversalZip = createZipWithEntry(
'..\\..\\..\\evil.txt',
'malicious content via backslash'
)
fs.writeFileSync(path.join(fixturesDir, 'backslash-traversal.zip'), backslashTraversalZip)
console.log('Created: test/fixtures/backslash-traversal.zip')
console.log('\nDone! Created malicious ZIP fixtures for path traversal testing.')