133 lines
3.7 KiB
JavaScript
133 lines
3.7 KiB
JavaScript
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}).`,
|
|
);
|
|
}
|
|
}
|