import { createHash } from "node:crypto"; const DOCUMENTED_UPSTREAM_PATH_ARTIFACTS = Object.freeze({ "engines/perl/emperl.data": "9529019418cf766a42cf2d25bd3fc97b47c9e689f5666cfc32dc11338d1b1e66", "engines/ruby/ruby.wasm": "fa1d5af1b4d601191739e6c4980d060d0d202181a7c83abf4930b3ca35b90f94", }); const LOCAL_PATH_PREFIXES = Object.freeze([ "/mnt/", "/Users/", "/home/", ...Array.from( { length: 26 }, (_, index) => `${String.fromCharCode(65 + index)}:\\Users\\`, ), ...Array.from( { length: 26 }, (_, index) => `${String.fromCharCode(65 + index)}:/Users/`, ), ...Array.from( { length: 26 }, (_, index) => `${String.fromCharCode(97 + index)}:\\Users\\`, ), ...Array.from( { length: 26 }, (_, index) => `${String.fromCharCode(97 + index)}:/Users/`, ), ]); function sha256(value) { return createHash("sha256").update(value).digest("hex"); } function utf16BigEndian(value) { const encoded = Buffer.from(value, "utf16le"); for (let index = 0; index < encoded.byteLength; index += 2) { const first = encoded[index]; encoded[index] = encoded[index + 1]; encoded[index + 1] = first; } return encoded; } function encodedForms(value) { return [ { encoding: "ASCII/UTF-8", value: Buffer.from(value, "utf8") }, { encoding: "UTF-16LE", value: Buffer.from(value, "utf16le") }, { encoding: "UTF-16BE", value: utf16BigEndian(value) }, ]; } function startsWithAt(data, value, offset) { return ( offset + value.byteLength <= data.byteLength && data.subarray(offset, offset + value.byteLength).equals(value) ); } function isAllowedVirtualHome(data, offset, encoding, allowedVirtualHomes) { return allowedVirtualHomes.some((home) => { const encodedHome = encodedForms(home).find( (candidate) => candidate.encoding === encoding, )?.value; if (!encodedHome || !startsWithAt(data, encodedHome, offset)) return false; const end = offset + encodedHome.byteLength; if (end === data.byteLength) return true; const character = encoding === "UTF-16LE" && end + 1 < data.byteLength ? data.readUInt16LE(end) : encoding === "UTF-16BE" && end + 1 < data.byteLength ? data.readUInt16BE(end) : data[end]; return ( character === 0 || character === 0x2f || !( (character >= 0x30 && character <= 0x39) || (character >= 0x41 && character <= 0x5a) || (character >= 0x61 && character <= 0x7a) || character === 0x2e || character === 0x5f || character === 0x2d ) ); }); } export function localAbsolutePathMatch( input, artifactName, { allowedVirtualHomes = [] } = {}, ) { const data = Buffer.from(input); const documentedDigest = DOCUMENTED_UPSTREAM_PATH_ARTIFACTS[artifactName]; if (documentedDigest && sha256(data) === documentedDigest) return null; for (const prefix of LOCAL_PATH_PREFIXES) { for (const encoded of encodedForms(prefix)) { let offset = data.indexOf(encoded.value); while (offset !== -1) { if ( prefix !== "/home/" || !isAllowedVirtualHome( data, offset, encoded.encoding, allowedVirtualHomes, ) ) { return { prefix, encoding: encoded.encoding }; } offset = data.indexOf(encoded.value, offset + encoded.value.byteLength); } } } return null; } export function assertNoUnexpectedLocalPaths( input, artifactName, options = {}, ) { const match = localAbsolutePathMatch(input, artifactName, options); if (match) { throw new Error( `Local absolute path detected in ${artifactName} (${match.encoding} ${match.prefix}).`, ); } }