fix(node-host): harden pnpm approval binding

This commit is contained in:
Peter Steinberger
2026-03-13 12:57:21 +00:00
parent af4731aa5f
commit 2f03de029c
4 changed files with 79 additions and 11 deletions

View File

@@ -40,6 +40,7 @@ type RuntimeFixture = {
initialBody: string;
expectedArgvIndex: number;
binName?: string;
binNames?: string[];
};
function createScriptOperandFixture(tmp: string, fixture?: RuntimeFixture): ScriptOperandFixture {
@@ -356,6 +357,20 @@ describe("hardenApprovedExecutionPaths", () => {
initialBody: 'console.log("SAFE");\n',
expectedArgvIndex: 3,
},
{
name: "pnpm reporter exec tsx file",
argv: ["pnpm", "--reporter", "silent", "exec", "tsx", "./run.ts"],
scriptName: "run.ts",
initialBody: 'console.log("SAFE");\n',
expectedArgvIndex: 5,
},
{
name: "pnpm reporter-equals exec tsx file",
argv: ["pnpm", "--reporter=silent", "exec", "tsx", "./run.ts"],
scriptName: "run.ts",
initialBody: 'console.log("SAFE");\n',
expectedArgvIndex: 4,
},
{
name: "pnpm js shim exec tsx file",
argv: ["./pnpm.js", "exec", "tsx", "./run.ts"],
@@ -370,6 +385,22 @@ describe("hardenApprovedExecutionPaths", () => {
initialBody: 'console.log("SAFE");\n',
expectedArgvIndex: 4,
},
{
name: "pnpm node file",
argv: ["pnpm", "node", "./run.js"],
scriptName: "run.js",
initialBody: 'console.log("SAFE");\n',
expectedArgvIndex: 2,
binNames: ["pnpm", "node"],
},
{
name: "pnpm node double-dash file",
argv: ["pnpm", "node", "--", "./run.js"],
scriptName: "run.js",
initialBody: 'console.log("SAFE");\n',
expectedArgvIndex: 3,
binNames: ["pnpm", "node"],
},
{
name: "npx tsx file",
argv: ["npx", "tsx", "./run.ts"],
@@ -395,9 +426,9 @@ describe("hardenApprovedExecutionPaths", () => {
for (const runtimeCase of mutableOperandCases) {
it(`captures mutable ${runtimeCase.name} operands in approval plans`, () => {
const binNames = runtimeCase.binName
? [runtimeCase.binName]
: ["bunx", "pnpm", "npm", "npx", "tsx"];
const binNames =
runtimeCase.binNames ??
(runtimeCase.binName ? [runtimeCase.binName] : ["bunx", "pnpm", "npm", "npx", "tsx"]);
withFakeRuntimeBins({
binNames,
run: () => {

View File

@@ -164,6 +164,26 @@ const NPM_EXEC_FLAG_OPTIONS = new Set([
"-y",
]);
const PNPM_OPTIONS_WITH_VALUE = new Set([
"--config",
"--dir",
"--filter",
"--reporter",
"--stream",
"--test-pattern",
"--workspace-concurrency",
"-C",
]);
const PNPM_FLAG_OPTIONS = new Set([
"--aggregate-output",
"--color",
"--recursive",
"--silent",
"--workspace-root",
"-r",
]);
type FileOperandCollection = {
hits: number[];
sawOptionValueFile: boolean;
@@ -299,6 +319,8 @@ function normalizePackageManagerExecToken(token: string): string {
if (!normalized) {
return normalized;
}
// Approval binding only promises best-effort recovery of the effective runtime
// command for common package-manager shims; it is not full package-manager semantics.
return normalized.replace(/\.(?:c|m)?js$/i, "");
}
@@ -315,17 +337,30 @@ function unwrapPnpmExecInvocation(argv: string[]): string[] | null {
continue;
}
if (!token.startsWith("-")) {
if (token !== "exec" || idx + 1 >= argv.length) {
return null;
if (token === "exec") {
if (idx + 1 >= argv.length) {
return null;
}
const tail = argv.slice(idx + 1);
return tail[0] === "--" ? (tail.length > 1 ? tail.slice(1) : null) : tail;
}
const tail = argv.slice(idx + 1);
return tail[0] === "--" ? (tail.length > 1 ? tail.slice(1) : null) : tail;
if (token === "node") {
const tail = argv.slice(idx + 1);
const normalizedTail = tail[0] === "--" ? tail.slice(1) : tail;
return ["node", ...normalizedTail];
}
return null;
}
if ((token === "-C" || token === "--dir" || token === "--filter") && !token.includes("=")) {
idx += 2;
const [flag] = token.toLowerCase().split("=", 2);
if (PNPM_OPTIONS_WITH_VALUE.has(flag)) {
idx += token.includes("=") ? 1 : 2;
continue;
}
idx += 1;
if (PNPM_FLAG_OPTIONS.has(flag)) {
idx += 1;
continue;
}
return null;
}
return null;
}