mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-14 10:52:17 +03:00
494 lines
18 KiB
TypeScript
494 lines
18 KiB
TypeScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { createHash } from "node:crypto";
|
|
import {
|
|
existsSync,
|
|
lstatSync,
|
|
mkdtempSync,
|
|
mkdirSync,
|
|
readFileSync,
|
|
renameSync,
|
|
rmSync,
|
|
symlinkSync,
|
|
writeFileSync,
|
|
} from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { basename, join } from "node:path";
|
|
|
|
import { fixTlsClientNodeBinary } from "../../scripts/build/fixTlsClientNodeBinary.mjs";
|
|
|
|
function makeRoot() {
|
|
return mkdtempSync(join(tmpdir(), "fix-tls-client-node-binary-security-"));
|
|
}
|
|
|
|
test("strict mode rejects a symlinked node_modules ancestor before recovery writes outside root", async () => {
|
|
const rootDir = makeRoot();
|
|
const outsideDir = makeRoot();
|
|
try {
|
|
const bytes = "verified-native-must-stay-inside-root";
|
|
const asset = {
|
|
file: "tls-client-linux-ubuntu-amd64-1.15.1.so",
|
|
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
};
|
|
const outsideNodeModules = join(outsideDir, "node_modules");
|
|
const tlsClientDir = join(outsideNodeModules, "tls-client-node");
|
|
const rootBin = join(tlsClientDir, "bin");
|
|
const scriptsDir = join(tlsClientDir, "scripts");
|
|
const outsideBinary = join(rootBin, asset.file);
|
|
const marker = join(tlsClientDir, ".postinstall-ran");
|
|
mkdirSync(rootBin, { recursive: true });
|
|
mkdirSync(scriptsDir, { recursive: true });
|
|
symlinkSync(outsideNodeModules, join(rootDir, "node_modules"), "dir");
|
|
writeFileSync(
|
|
join(scriptsDir, "postinstall.js"),
|
|
`require("fs").writeFileSync(${JSON.stringify(marker)}, "ran");
|
|
require("fs").writeFileSync(${JSON.stringify(outsideBinary)}, ${JSON.stringify(bytes)});`
|
|
);
|
|
|
|
await assert.rejects(
|
|
fixTlsClientNodeBinary({
|
|
rootDir,
|
|
asset,
|
|
strict: true,
|
|
retryDelaysMs: [],
|
|
log() {},
|
|
}),
|
|
/unsafe.*(ancestor|outside)|symlink/i
|
|
);
|
|
assert.equal(existsSync(marker), false);
|
|
assert.equal(existsSync(outsideBinary), false);
|
|
} finally {
|
|
rmSync(rootDir, { recursive: true, force: true });
|
|
rmSync(outsideDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("strict mode revalidates root ancestors after fetch before writing recovered bytes", async () => {
|
|
const rootDir = makeRoot();
|
|
const outsideDir = makeRoot();
|
|
try {
|
|
const bytes = Buffer.from("verified-native-after-ancestor-race");
|
|
const asset = {
|
|
file: "tls-client-race-test.so",
|
|
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
};
|
|
const rootNodeModules = join(rootDir, "node_modules");
|
|
const originalNodeModules = join(rootDir, "node_modules-before-race");
|
|
const outsideNodeModules = join(outsideDir, "node_modules");
|
|
const outsideBin = join(outsideNodeModules, "tls-client-node", "bin");
|
|
const outsideBinary = join(outsideBin, asset.file);
|
|
mkdirSync(join(rootNodeModules, "tls-client-node", "bin"), { recursive: true });
|
|
mkdirSync(outsideBin, { recursive: true });
|
|
|
|
const targetArch = process.arch === "arm64" ? "x64" : "arm64";
|
|
await assert.rejects(
|
|
fixTlsClientNodeBinary({
|
|
rootDir,
|
|
asset,
|
|
platform: process.platform,
|
|
arches: [targetArch],
|
|
fetchImpl: async () => {
|
|
renameSync(rootNodeModules, originalNodeModules);
|
|
symlinkSync(outsideNodeModules, rootNodeModules, "dir");
|
|
return new Response(bytes, { status: 200 });
|
|
},
|
|
strict: true,
|
|
retryDelaysMs: [],
|
|
log() {},
|
|
}),
|
|
/unsafe.*(ancestor|outside)|symlink/i
|
|
);
|
|
|
|
assert.equal(existsSync(outsideBinary), false);
|
|
} finally {
|
|
rmSync(rootDir, { recursive: true, force: true });
|
|
rmSync(outsideDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("strict mode revalidates root ancestors before recovery can remove an external file", async () => {
|
|
const rootDir = makeRoot();
|
|
const outsideDir = makeRoot();
|
|
try {
|
|
const bytes = Buffer.from("verified-native-after-pre-recovery-check");
|
|
const asset = {
|
|
file: "tls-client-pre-recovery-race-test.so",
|
|
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
};
|
|
const rootNodeModules = join(rootDir, "node_modules");
|
|
const originalNodeModules = join(rootDir, "node_modules-before-pre-recovery-race");
|
|
const rootBin = join(rootNodeModules, "tls-client-node", "bin");
|
|
const outsideNodeModules = join(outsideDir, "node_modules");
|
|
const outsideBin = join(outsideNodeModules, "tls-client-node", "bin");
|
|
const outsideBinary = join(outsideBin, asset.file);
|
|
mkdirSync(rootBin, { recursive: true });
|
|
mkdirSync(outsideBin, { recursive: true });
|
|
writeFileSync(join(rootBin, asset.file), "tampered-root-binary");
|
|
writeFileSync(outsideBinary, "external-file-must-not-be-removed");
|
|
|
|
let swappedAncestor = false;
|
|
await assert.rejects(
|
|
fixTlsClientNodeBinary({
|
|
rootDir,
|
|
asset,
|
|
fetchImpl: async () => new Response(bytes, { status: 200 }),
|
|
strict: true,
|
|
retryDelaysMs: [],
|
|
log(message) {
|
|
if (!swappedAncestor && message.includes("missing or unverified")) {
|
|
renameSync(rootNodeModules, originalNodeModules);
|
|
symlinkSync(outsideNodeModules, rootNodeModules, "dir");
|
|
swappedAncestor = true;
|
|
}
|
|
},
|
|
}),
|
|
/unsafe.*(ancestor|outside)|symlink/i
|
|
);
|
|
assert.equal(swappedAncestor, true);
|
|
assert.equal(readFileSync(outsideBinary, "utf8"), "external-file-must-not-be-removed");
|
|
} finally {
|
|
rmSync(rootDir, { recursive: true, force: true });
|
|
rmSync(outsideDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("strict mode revalidates root ancestors after recovery before post-verification", async () => {
|
|
const rootDir = makeRoot();
|
|
const outsideDir = makeRoot();
|
|
try {
|
|
const bytes = Buffer.from("verified-native-before-post-recovery-race");
|
|
const asset = {
|
|
file: "tls-client-post-recovery-race-test.so",
|
|
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
};
|
|
const rootNodeModules = join(rootDir, "node_modules");
|
|
const originalNodeModules = join(rootDir, "node_modules-before-post-recovery-race");
|
|
const outsideNodeModules = join(outsideDir, "node_modules");
|
|
const outsideBin = join(outsideNodeModules, "tls-client-node", "bin");
|
|
mkdirSync(join(rootNodeModules, "tls-client-node", "bin"), { recursive: true });
|
|
mkdirSync(outsideBin, { recursive: true });
|
|
writeFileSync(join(outsideBin, asset.file), bytes);
|
|
|
|
const targetArch = process.arch === "arm64" ? "x64" : "arm64";
|
|
let swappedAncestor = false;
|
|
await assert.rejects(
|
|
fixTlsClientNodeBinary({
|
|
rootDir,
|
|
asset,
|
|
platform: process.platform,
|
|
arches: [targetArch],
|
|
fetchImpl: async () => new Response(bytes, { status: 200 }),
|
|
strict: true,
|
|
retryDelaysMs: [],
|
|
log(message) {
|
|
if (message.includes("fetched successfully")) {
|
|
renameSync(rootNodeModules, originalNodeModules);
|
|
symlinkSync(outsideNodeModules, rootNodeModules, "dir");
|
|
swappedAncestor = true;
|
|
}
|
|
},
|
|
}),
|
|
/unsafe.*(ancestor|outside)|symlink/i
|
|
);
|
|
assert.equal(swappedAncestor, true);
|
|
} finally {
|
|
rmSync(rootDir, { recursive: true, force: true });
|
|
rmSync(outsideDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("strict mode revalidates dist ancestors before copying verified bytes", async () => {
|
|
const rootDir = makeRoot();
|
|
const outsideDir = makeRoot();
|
|
try {
|
|
const bytes = Buffer.from("verified-native-before-dist-race");
|
|
const asset = {
|
|
file: "tls-client-dist-race-test.so",
|
|
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
};
|
|
const rootBin = join(rootDir, "node_modules", "tls-client-node", "bin");
|
|
const rootDist = join(rootDir, "dist");
|
|
const originalDist = join(rootDir, "dist-before-race");
|
|
const outsideDist = join(outsideDir, "dist-target");
|
|
const outsideTlsClientDir = join(outsideDist, "node_modules", "tls-client-node");
|
|
const outsideBinary = join(outsideTlsClientDir, "bin", asset.file);
|
|
mkdirSync(rootBin, { recursive: true });
|
|
mkdirSync(join(rootDist, "node_modules", "tls-client-node"), { recursive: true });
|
|
mkdirSync(outsideTlsClientDir, { recursive: true });
|
|
writeFileSync(join(rootBin, asset.file), bytes);
|
|
|
|
let swappedAncestor = false;
|
|
await assert.rejects(
|
|
fixTlsClientNodeBinary({
|
|
rootDir,
|
|
asset,
|
|
strict: true,
|
|
retryDelaysMs: [],
|
|
afterSourceStat() {
|
|
if (!swappedAncestor) {
|
|
renameSync(rootDist, originalDist);
|
|
symlinkSync(outsideDist, rootDist, "dir");
|
|
swappedAncestor = true;
|
|
}
|
|
},
|
|
log() {},
|
|
}),
|
|
/unsafe.*(ancestor|outside)|symlink/i
|
|
);
|
|
assert.equal(swappedAncestor, true);
|
|
assert.equal(existsSync(outsideBinary), false);
|
|
} finally {
|
|
rmSync(rootDir, { recursive: true, force: true });
|
|
rmSync(outsideDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("strict mode rejects unmanifested native siblings in every packaged bin root", async () => {
|
|
const locations: Array<{
|
|
label: string;
|
|
binDir: (rootDir: string, standaloneDir: string) => string;
|
|
nextDistDir?: string | null;
|
|
}> = [
|
|
{
|
|
label: "root node_modules",
|
|
binDir: (rootDir: string, _standaloneDir: string) =>
|
|
join(rootDir, "node_modules", "tls-client-node", "bin"),
|
|
},
|
|
{
|
|
label: "dist node_modules",
|
|
binDir: (rootDir: string, _standaloneDir: string) =>
|
|
join(rootDir, "dist", "node_modules", "tls-client-node", "bin"),
|
|
},
|
|
{
|
|
label: "standalone node_modules",
|
|
binDir: (_rootDir: string, standaloneDir: string) =>
|
|
join(standaloneDir, "node_modules", "tls-client-node", "bin"),
|
|
},
|
|
{
|
|
label: "projects OmniRoute nested standalone node_modules",
|
|
binDir: (_rootDir: string, standaloneDir: string) =>
|
|
join(standaloneDir, "projects", "OmniRoute", "node_modules", "tls-client-node", "bin"),
|
|
},
|
|
{
|
|
label: "root-basename nested standalone node_modules",
|
|
binDir: (rootDir: string, standaloneDir: string) =>
|
|
join(standaloneDir, basename(rootDir), "node_modules", "tls-client-node", "bin"),
|
|
},
|
|
{
|
|
label: "standalone runtime assets",
|
|
binDir: (_rootDir: string, standaloneDir: string) =>
|
|
join(standaloneDir, "runtime-assets", "tls-client", "bin"),
|
|
},
|
|
{
|
|
label: "isolated next build node_modules",
|
|
nextDistDir: null,
|
|
binDir: (rootDir: string, _standaloneDir: string) =>
|
|
join(rootDir, ".build", "next", "node_modules", "tls-client-node", "bin"),
|
|
},
|
|
{
|
|
label: "configured isolated next build node_modules",
|
|
nextDistDir: ".custom-next",
|
|
binDir: (rootDir: string, _standaloneDir: string) =>
|
|
join(rootDir, ".custom-next", "node_modules", "tls-client-node", "bin"),
|
|
},
|
|
{
|
|
label: "nested standalone next build node_modules",
|
|
nextDistDir: null,
|
|
binDir: (_rootDir: string, standaloneDir: string) =>
|
|
join(standaloneDir, ".build", "next", "node_modules", "tls-client-node", "bin"),
|
|
},
|
|
{
|
|
label: "configured nested standalone next build node_modules",
|
|
nextDistDir: ".custom-next",
|
|
binDir: (_rootDir: string, standaloneDir: string) =>
|
|
join(standaloneDir, ".custom-next", "node_modules", "tls-client-node", "bin"),
|
|
},
|
|
];
|
|
|
|
for (const location of locations) {
|
|
const rootDir = makeRoot();
|
|
const hadNextDistDir = Object.prototype.hasOwnProperty.call(process.env, "NEXT_DIST_DIR");
|
|
const previousNextDistDir = process.env.NEXT_DIST_DIR;
|
|
try {
|
|
if (location.nextDistDir === null) delete process.env.NEXT_DIST_DIR;
|
|
else if (location.nextDistDir) process.env.NEXT_DIST_DIR = location.nextDistDir;
|
|
const binary = Buffer.from(`verified-target-for-${location.label}`);
|
|
const asset = {
|
|
file: "tls-client-target-test.so",
|
|
sha256: createHash("sha256").update(binary).digest("hex"),
|
|
};
|
|
const rootBin = join(rootDir, "node_modules", "tls-client-node", "bin");
|
|
const standaloneDir = join(rootDir, ".build", "next", "standalone");
|
|
mkdirSync(rootBin, { recursive: true });
|
|
mkdirSync(standaloneDir, { recursive: true });
|
|
writeFileSync(join(rootBin, asset.file), binary);
|
|
const candidateBin = location.binDir(rootDir, standaloneDir);
|
|
mkdirSync(candidateBin, { recursive: true });
|
|
writeFileSync(join(candidateBin, "unlisted-native.so"), "must-not-ship");
|
|
|
|
await assert.rejects(
|
|
fixTlsClientNodeBinary({
|
|
rootDir,
|
|
asset,
|
|
standaloneDir,
|
|
requireStandalone: true,
|
|
strict: true,
|
|
retryDelaysMs: [],
|
|
fetchImpl: async () => {
|
|
throw new Error("verified root target must not trigger recovery");
|
|
},
|
|
log() {},
|
|
}),
|
|
/unlisted|not.*manifest|unexpected native sibling/i,
|
|
location.label
|
|
);
|
|
} finally {
|
|
if (hadNextDistDir) process.env.NEXT_DIST_DIR = previousNextDistDir;
|
|
else delete process.env.NEXT_DIST_DIR;
|
|
rmSync(rootDir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
});
|
|
|
|
test("strict mode rejects a manifest-known non-target sibling with an invalid digest", async () => {
|
|
const rootDir = makeRoot();
|
|
try {
|
|
const targetBytes = Buffer.from("verified-linux-x64-target");
|
|
const siblingBytes = Buffer.from("verified-linux-arm64-sibling");
|
|
const targetAsset = {
|
|
file: "tls-client-linux-x64-test.so",
|
|
sha256: createHash("sha256").update(targetBytes).digest("hex"),
|
|
};
|
|
const siblingAsset = {
|
|
file: "tls-client-linux-arm64-test.so",
|
|
sha256: createHash("sha256").update(siblingBytes).digest("hex"),
|
|
};
|
|
const nativeAssets = {
|
|
"linux-x64": targetAsset,
|
|
"linux-arm64": siblingAsset,
|
|
};
|
|
const rootBin = join(rootDir, "node_modules", "tls-client-node", "bin");
|
|
mkdirSync(rootBin, { recursive: true });
|
|
writeFileSync(join(rootBin, targetAsset.file), targetBytes);
|
|
writeFileSync(join(rootBin, siblingAsset.file), "tampered-known-sibling");
|
|
|
|
await assert.rejects(
|
|
fixTlsClientNodeBinary({
|
|
rootDir,
|
|
platform: "linux",
|
|
arches: ["x64"],
|
|
nativeAssets,
|
|
strict: true,
|
|
retryDelaysMs: [],
|
|
fetchImpl: async () => {
|
|
throw new Error("verified target must not trigger recovery");
|
|
},
|
|
log() {},
|
|
}),
|
|
/unverified|sha-?256|digest/i
|
|
);
|
|
} finally {
|
|
rmSync(rootDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("strict mode allows a verified subset of manifest-known native siblings", async () => {
|
|
const rootDir = makeRoot();
|
|
try {
|
|
const targetBytes = Buffer.from("verified-subset-linux-x64");
|
|
const siblingBytes = Buffer.from("verified-subset-linux-arm64");
|
|
const targetAsset = {
|
|
file: "tls-client-subset-linux-x64.so",
|
|
sha256: createHash("sha256").update(targetBytes).digest("hex"),
|
|
};
|
|
const siblingAsset = {
|
|
file: "tls-client-subset-linux-arm64.so",
|
|
sha256: createHash("sha256").update(siblingBytes).digest("hex"),
|
|
};
|
|
const nativeAssets = {
|
|
"linux-x64": targetAsset,
|
|
"linux-arm64": siblingAsset,
|
|
};
|
|
const rootBin = join(rootDir, "node_modules", "tls-client-node", "bin");
|
|
const targetPath = join(rootBin, targetAsset.file);
|
|
const siblingPath = join(rootBin, siblingAsset.file);
|
|
mkdirSync(rootBin, { recursive: true });
|
|
writeFileSync(targetPath, targetBytes);
|
|
writeFileSync(siblingPath, siblingBytes);
|
|
|
|
let fetchCalls = 0;
|
|
await fixTlsClientNodeBinary({
|
|
rootDir,
|
|
platform: "linux",
|
|
arches: ["x64"],
|
|
nativeAssets,
|
|
strict: true,
|
|
retryDelaysMs: [],
|
|
fetchImpl: async () => {
|
|
fetchCalls += 1;
|
|
throw new Error("verified subset must not trigger recovery");
|
|
},
|
|
log() {},
|
|
});
|
|
|
|
assert.equal(fetchCalls, 0);
|
|
assert.deepEqual(readFileSync(targetPath), targetBytes);
|
|
assert.deepEqual(readFileSync(siblingPath), siblingBytes);
|
|
if (process.platform !== "win32") {
|
|
assert.equal(lstatSync(targetPath).mode & 0o777, 0o555);
|
|
assert.equal(lstatSync(siblingPath).mode & 0o777, 0o555);
|
|
}
|
|
} finally {
|
|
rmSync(rootDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("strict mode rejects manifest-known siblings that are symlinks or non-files", async () => {
|
|
for (const variant of ["symlink", "directory"] as const) {
|
|
const rootDir = makeRoot();
|
|
try {
|
|
const targetBytes = Buffer.from(`verified-target-for-${variant}`);
|
|
const siblingBytes = Buffer.from(`verified-sibling-for-${variant}`);
|
|
const targetAsset = {
|
|
file: `tls-client-target-${variant}.so`,
|
|
sha256: createHash("sha256").update(targetBytes).digest("hex"),
|
|
};
|
|
const siblingAsset = {
|
|
file: `tls-client-sibling-${variant}.so`,
|
|
sha256: createHash("sha256").update(siblingBytes).digest("hex"),
|
|
};
|
|
const nativeAssets = {
|
|
"linux-x64": targetAsset,
|
|
"linux-arm64": siblingAsset,
|
|
};
|
|
const rootBin = join(rootDir, "node_modules", "tls-client-node", "bin");
|
|
const siblingPath = join(rootBin, siblingAsset.file);
|
|
mkdirSync(rootBin, { recursive: true });
|
|
writeFileSync(join(rootBin, targetAsset.file), targetBytes);
|
|
if (variant === "symlink") {
|
|
const outsideFile = join(rootDir, "outside-native.so");
|
|
writeFileSync(outsideFile, siblingBytes);
|
|
symlinkSync(outsideFile, siblingPath);
|
|
} else {
|
|
mkdirSync(siblingPath);
|
|
}
|
|
|
|
await assert.rejects(
|
|
fixTlsClientNodeBinary({
|
|
rootDir,
|
|
platform: "linux",
|
|
arches: ["x64"],
|
|
nativeAssets,
|
|
strict: true,
|
|
retryDelaysMs: [],
|
|
log() {},
|
|
}),
|
|
/unsafe.*sibling|symlink|non-regular file/i,
|
|
variant
|
|
);
|
|
} finally {
|
|
rmSync(rootDir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
});
|