Pre-installation verification of VS Code extensions via URL — fetch a remote VSIX, extract it in a hardened sandbox, and run the existing IDE scanner pipeline against it. No npm dependencies. Sources: - VS Code Marketplace (publisher.gallery.vsassets.io direct download) - OpenVSX (open-vsx.org official API) - Direct .vsix HTTPS URLs Defenses: - HTTPS-only, TLS verified, manual redirect with per-source host whitelist - 30s total timeout via AbortController - 50MB compressed cap, 500MB uncompressed, 100x expansion ratio - Zero-dep ZIP extractor: zip-slip, absolute paths, drive letters, NUL bytes, symlinks (Unix mode 0xA000), depth limits, ZIP64 rejected, encrypted rejected - SHA-256 streamed during fetch, surfaced in meta.source - Temp dir cleanup in all paths (try/finally) Files: - scanners/lib/vsix-fetch.mjs (HTTPS fetcher, host whitelist, streaming SHA-256) - scanners/lib/zip-extract.mjs (zero-dep parser with hardening caps) - knowledge/marketplace-api-notes.md (endpoint reference) - 3 test files (48 tests added: vsix-fetch, zip-extract, ide-extension-url) Tests: 1296 → 1344 (all green). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
97 lines
3.3 KiB
JavaScript
97 lines
3.3 KiB
JavaScript
// build-zip.mjs — Minimal synthetic ZIP builder for tests.
|
|
// Supports STORE method only. Lets tests construct adversarial archives that
|
|
// real zip tools refuse to emit (zip-slip names, symlink mode bits, oversized
|
|
// uncompressed sizes for bomb tests).
|
|
|
|
import { crc32 } from 'node:zlib';
|
|
|
|
const SIG_LFH = 0x04034b50;
|
|
const SIG_CD = 0x02014b50;
|
|
const SIG_EOCD = 0x06054b50;
|
|
|
|
function crc(buf) {
|
|
return crc32(buf) >>> 0;
|
|
}
|
|
|
|
/**
|
|
* Build a ZIP buffer from a list of entries.
|
|
* @param {Array<{ name: string, data: Buffer|string, externalAttr?: number, versionMadeBy?: number, declaredUncompSize?: number, declaredCompSize?: number }>} entries
|
|
* @returns {Buffer}
|
|
*/
|
|
export function buildZip(entries) {
|
|
const lfhParts = [];
|
|
const cdParts = [];
|
|
let offset = 0;
|
|
|
|
for (const entry of entries) {
|
|
const nameBuf = Buffer.from(entry.name, 'utf8');
|
|
const data = Buffer.isBuffer(entry.data) ? entry.data : Buffer.from(entry.data || '', 'utf8');
|
|
const compSize = entry.declaredCompSize ?? data.length;
|
|
const uncompSize = entry.declaredUncompSize ?? data.length;
|
|
const c = crc(data);
|
|
|
|
// Local file header (30 bytes)
|
|
const lfh = Buffer.alloc(30);
|
|
lfh.writeUInt32LE(SIG_LFH, 0);
|
|
lfh.writeUInt16LE(20, 4); // version needed
|
|
lfh.writeUInt16LE(0, 6); // flags
|
|
lfh.writeUInt16LE(0, 8); // method = STORE
|
|
lfh.writeUInt16LE(0, 10); // time
|
|
lfh.writeUInt16LE(0, 12); // date
|
|
lfh.writeUInt32LE(c, 14); // crc32
|
|
lfh.writeUInt32LE(compSize, 18); // compressed size
|
|
lfh.writeUInt32LE(uncompSize, 22); // uncompressed size
|
|
lfh.writeUInt16LE(nameBuf.length, 26);
|
|
lfh.writeUInt16LE(0, 28); // extra len
|
|
|
|
lfhParts.push(lfh, nameBuf, data);
|
|
const thisLfhOffset = offset;
|
|
offset += lfh.length + nameBuf.length + data.length;
|
|
|
|
// Central directory header (46 bytes)
|
|
const cd = Buffer.alloc(46);
|
|
cd.writeUInt32LE(SIG_CD, 0);
|
|
cd.writeUInt16LE(entry.versionMadeBy ?? (3 << 8) | 20, 4); // OS=Unix(3), version=20
|
|
cd.writeUInt16LE(20, 6);
|
|
cd.writeUInt16LE(0, 8);
|
|
cd.writeUInt16LE(0, 10);
|
|
cd.writeUInt16LE(0, 12);
|
|
cd.writeUInt16LE(0, 14);
|
|
cd.writeUInt32LE(c, 16);
|
|
cd.writeUInt32LE(compSize, 20);
|
|
cd.writeUInt32LE(uncompSize, 24);
|
|
cd.writeUInt16LE(nameBuf.length, 28);
|
|
cd.writeUInt16LE(0, 30);
|
|
cd.writeUInt16LE(0, 32); // comment len
|
|
cd.writeUInt16LE(0, 34); // disk start
|
|
cd.writeUInt16LE(0, 36); // internal attrs
|
|
cd.writeUInt32LE((entry.externalAttr ?? 0) >>> 0, 38); // external attrs (unsigned)
|
|
cd.writeUInt32LE(thisLfhOffset, 42);
|
|
|
|
cdParts.push(cd, nameBuf);
|
|
}
|
|
|
|
const lfhSection = Buffer.concat(lfhParts);
|
|
const cdSection = Buffer.concat(cdParts);
|
|
const cdOffset = lfhSection.length;
|
|
const cdSize = cdSection.length;
|
|
|
|
const eocd = Buffer.alloc(22);
|
|
eocd.writeUInt32LE(SIG_EOCD, 0);
|
|
eocd.writeUInt16LE(0, 4);
|
|
eocd.writeUInt16LE(0, 6);
|
|
eocd.writeUInt16LE(entries.length, 8);
|
|
eocd.writeUInt16LE(entries.length, 10);
|
|
eocd.writeUInt32LE(cdSize, 12);
|
|
eocd.writeUInt32LE(cdOffset, 16);
|
|
eocd.writeUInt16LE(0, 20);
|
|
|
|
return Buffer.concat([lfhSection, cdSection, eocd]);
|
|
}
|
|
|
|
/** Convenience: produce a unix mode in the upper 16 bits of externalAttr. */
|
|
export function unixModeAttr(mode) {
|
|
return (mode & 0xFFFF) << 16;
|
|
}
|
|
|
|
export const MODE_SYMLINK = 0xA1FF; // S_IFLNK | rwxrwxrwx
|