mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 11:48:38 +00:00
fix(node-host): harden ambiguous approval operand binding (#44247)
* fix(node-host): harden approval operand binding * test(node-host): cover approval parser hardening * docs(changelog): note approval hardening GHSA cluster * Update CHANGELOG.md * fix(node-host): remove dead approval parser entries * test(node-host): cover bunx approval wrapper * fix(node-host): unwrap pnpm shim exec forms * test(node-host): cover pnpm shim wrappers
This commit is contained in:
@@ -44,6 +44,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Security/Feishu reactions: preserve looked-up group chat typing and fail closed on ambiguous reaction context so group authorization and mention gating cannot be bypassed through synthetic `p2p` reactions. (`GHSA-m69h-jm2f-2pv8`)(#44088) Thanks @zpbrent and @vincentkoc.
|
- Security/Feishu reactions: preserve looked-up group chat typing and fail closed on ambiguous reaction context so group authorization and mention gating cannot be bypassed through synthetic `p2p` reactions. (`GHSA-m69h-jm2f-2pv8`)(#44088) Thanks @zpbrent and @vincentkoc.
|
||||||
- Security/LINE webhook: require signatures for empty-event POST probes too so unsigned requests no longer confirm webhook reachability with a `200` response. (`GHSA-mhxh-9pjm-w7q5`)(#44090) Thanks @TerminalsandCoffee and @vincentkoc.
|
- Security/LINE webhook: require signatures for empty-event POST probes too so unsigned requests no longer confirm webhook reachability with a `200` response. (`GHSA-mhxh-9pjm-w7q5`)(#44090) Thanks @TerminalsandCoffee and @vincentkoc.
|
||||||
- Security/Zalo webhook: rate limit invalid secret guesses before auth so weak webhook secrets cannot be brute-forced through unauthenticated churned requests without pre-auth `429` responses. (`GHSA-5m9r-p9g7-679c`)(#44173) Thanks @zpbrent and @vincentkoc.
|
- Security/Zalo webhook: rate limit invalid secret guesses before auth so weak webhook secrets cannot be brute-forced through unauthenticated churned requests without pre-auth `429` responses. (`GHSA-5m9r-p9g7-679c`)(#44173) Thanks @zpbrent and @vincentkoc.
|
||||||
|
- Security/exec approvals: fail closed for ambiguous inline loader and shell-payload script execution, bind the real script after POSIX shell value-taking flags, and unwrap `pnpm`/`npm exec`/`npx` script runners before approval binding. (`GHSA-57jw-9722-6rf2`)(`GHSA-jvqh-rfmh-jh27`)(`GHSA-x7pp-23xv-mmr4`)(`GHSA-jc5j-vg4r-j5jx`)(#44247) Thanks @tdjackey and @vincentkoc.
|
||||||
- Doctor/gateway service audit: canonicalize service entrypoint paths before comparing them so symlink-vs-realpath installs no longer trigger false "entrypoint does not match the current install" repair prompts. (#43882) Thanks @ngutman.
|
- Doctor/gateway service audit: canonicalize service entrypoint paths before comparing them so symlink-vs-realpath installs no longer trigger false "entrypoint does not match the current install" repair prompts. (#43882) Thanks @ngutman.
|
||||||
- Doctor/gateway service audit: earlier groundwork for this fix landed in the superseded #28338 branch. Thanks @realriphub.
|
- Doctor/gateway service audit: earlier groundwork for this fix landed in the superseded #28338 branch. Thanks @realriphub.
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { formatExecCommand } from "../infra/system-run-command.js";
|
|||||||
import {
|
import {
|
||||||
buildSystemRunApprovalPlan,
|
buildSystemRunApprovalPlan,
|
||||||
hardenApprovedExecutionPaths,
|
hardenApprovedExecutionPaths,
|
||||||
|
resolveMutableFileOperandSnapshotSync,
|
||||||
} from "./invoke-system-run-plan.js";
|
} from "./invoke-system-run-plan.js";
|
||||||
|
|
||||||
type PathTokenSetup = {
|
type PathTokenSetup = {
|
||||||
@@ -94,6 +95,36 @@ function withFakeRuntimeBin<T>(params: { binName: string; run: () => T }): T {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function withFakeRuntimeBins<T>(params: { binNames: string[]; run: () => T }): T {
|
||||||
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-runtime-bins-"));
|
||||||
|
const binDir = path.join(tmp, "bin");
|
||||||
|
fs.mkdirSync(binDir, { recursive: true });
|
||||||
|
for (const binName of params.binNames) {
|
||||||
|
const runtimePath =
|
||||||
|
process.platform === "win32"
|
||||||
|
? path.join(binDir, `${binName}.cmd`)
|
||||||
|
: path.join(binDir, binName);
|
||||||
|
const runtimeBody =
|
||||||
|
process.platform === "win32" ? "@echo off\r\nexit /b 0\r\n" : "#!/bin/sh\nexit 0\n";
|
||||||
|
fs.writeFileSync(runtimePath, runtimeBody, { mode: 0o755 });
|
||||||
|
if (process.platform !== "win32") {
|
||||||
|
fs.chmodSync(runtimePath, 0o755);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const oldPath = process.env.PATH;
|
||||||
|
process.env.PATH = `${binDir}${path.delimiter}${oldPath ?? ""}`;
|
||||||
|
try {
|
||||||
|
return params.run();
|
||||||
|
} finally {
|
||||||
|
if (oldPath === undefined) {
|
||||||
|
delete process.env.PATH;
|
||||||
|
} else {
|
||||||
|
process.env.PATH = oldPath;
|
||||||
|
}
|
||||||
|
fs.rmSync(tmp, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
describe("hardenApprovedExecutionPaths", () => {
|
describe("hardenApprovedExecutionPaths", () => {
|
||||||
const cases: HardeningCase[] = [
|
const cases: HardeningCase[] = [
|
||||||
{
|
{
|
||||||
@@ -318,16 +349,67 @@ describe("hardenApprovedExecutionPaths", () => {
|
|||||||
initialBody: 'console.log("SAFE");\n',
|
initialBody: 'console.log("SAFE");\n',
|
||||||
expectedArgvIndex: 2,
|
expectedArgvIndex: 2,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "pnpm exec tsx file",
|
||||||
|
argv: ["pnpm", "exec", "tsx", "./run.ts"],
|
||||||
|
scriptName: "run.ts",
|
||||||
|
initialBody: 'console.log("SAFE");\n',
|
||||||
|
expectedArgvIndex: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pnpm js shim exec tsx file",
|
||||||
|
argv: ["./pnpm.js", "exec", "tsx", "./run.ts"],
|
||||||
|
scriptName: "run.ts",
|
||||||
|
initialBody: 'console.log("SAFE");\n',
|
||||||
|
expectedArgvIndex: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pnpm exec double-dash tsx file",
|
||||||
|
argv: ["pnpm", "exec", "--", "tsx", "./run.ts"],
|
||||||
|
scriptName: "run.ts",
|
||||||
|
initialBody: 'console.log("SAFE");\n',
|
||||||
|
expectedArgvIndex: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "npx tsx file",
|
||||||
|
argv: ["npx", "tsx", "./run.ts"],
|
||||||
|
scriptName: "run.ts",
|
||||||
|
initialBody: 'console.log("SAFE");\n',
|
||||||
|
expectedArgvIndex: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bunx tsx file",
|
||||||
|
argv: ["bunx", "tsx", "./run.ts"],
|
||||||
|
scriptName: "run.ts",
|
||||||
|
initialBody: 'console.log("SAFE");\n',
|
||||||
|
expectedArgvIndex: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "npm exec tsx file",
|
||||||
|
argv: ["npm", "exec", "--", "tsx", "./run.ts"],
|
||||||
|
scriptName: "run.ts",
|
||||||
|
initialBody: 'console.log("SAFE");\n',
|
||||||
|
expectedArgvIndex: 4,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const runtimeCase of mutableOperandCases) {
|
for (const runtimeCase of mutableOperandCases) {
|
||||||
it(`captures mutable ${runtimeCase.name} operands in approval plans`, () => {
|
it(`captures mutable ${runtimeCase.name} operands in approval plans`, () => {
|
||||||
withFakeRuntimeBin({
|
const binNames = runtimeCase.binName
|
||||||
binName: runtimeCase.binName!,
|
? [runtimeCase.binName]
|
||||||
|
: ["bunx", "pnpm", "npm", "npx", "tsx"];
|
||||||
|
withFakeRuntimeBins({
|
||||||
|
binNames,
|
||||||
run: () => {
|
run: () => {
|
||||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-approval-script-plan-"));
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-approval-script-plan-"));
|
||||||
const fixture = createScriptOperandFixture(tmp, runtimeCase);
|
const fixture = createScriptOperandFixture(tmp, runtimeCase);
|
||||||
fs.writeFileSync(fixture.scriptPath, fixture.initialBody);
|
fs.writeFileSync(fixture.scriptPath, fixture.initialBody);
|
||||||
|
const executablePath = fixture.command[0];
|
||||||
|
if (executablePath?.endsWith("pnpm.js")) {
|
||||||
|
const shimPath = path.join(tmp, "pnpm.js");
|
||||||
|
fs.writeFileSync(shimPath, "#!/usr/bin/env node\nconsole.log('shim')\n");
|
||||||
|
fs.chmodSync(shimPath, 0o755);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const prepared = buildSystemRunApprovalPlan({
|
const prepared = buildSystemRunApprovalPlan({
|
||||||
command: fixture.command,
|
command: fixture.command,
|
||||||
@@ -441,4 +523,75 @@ describe("hardenApprovedExecutionPaths", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("rejects node inline import operands that cannot be bound to one stable file", () => {
|
||||||
|
withFakeRuntimeBin({
|
||||||
|
binName: "node",
|
||||||
|
run: () => {
|
||||||
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-node-import-inline-"));
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(path.join(tmp, "main.mjs"), 'console.log("SAFE")\n');
|
||||||
|
fs.writeFileSync(path.join(tmp, "preload.mjs"), 'console.log("SAFE")\n');
|
||||||
|
const prepared = buildSystemRunApprovalPlan({
|
||||||
|
command: ["node", "--import=./preload.mjs", "./main.mjs"],
|
||||||
|
cwd: tmp,
|
||||||
|
});
|
||||||
|
expect(prepared).toEqual({
|
||||||
|
ok: false,
|
||||||
|
message:
|
||||||
|
"SYSTEM_RUN_DENIED: approval cannot safely bind this interpreter/runtime command",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(tmp, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects shell payloads that hide mutable interpreter scripts", () => {
|
||||||
|
withFakeRuntimeBin({
|
||||||
|
binName: "node",
|
||||||
|
run: () => {
|
||||||
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-inline-shell-node-"));
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(path.join(tmp, "run.js"), 'console.log("SAFE")\n');
|
||||||
|
const prepared = buildSystemRunApprovalPlan({
|
||||||
|
command: ["sh", "-lc", "node ./run.js"],
|
||||||
|
cwd: tmp,
|
||||||
|
});
|
||||||
|
expect(prepared).toEqual({
|
||||||
|
ok: false,
|
||||||
|
message:
|
||||||
|
"SYSTEM_RUN_DENIED: approval cannot safely bind this interpreter/runtime command",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(tmp, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("captures the real shell script operand after value-taking shell flags", () => {
|
||||||
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-shell-option-value-"));
|
||||||
|
try {
|
||||||
|
const scriptPath = path.join(tmp, "run.sh");
|
||||||
|
fs.writeFileSync(scriptPath, "#!/bin/sh\necho SAFE\n");
|
||||||
|
fs.writeFileSync(path.join(tmp, "errexit"), "decoy\n");
|
||||||
|
const snapshot = resolveMutableFileOperandSnapshotSync({
|
||||||
|
argv: ["/bin/bash", "-o", "errexit", "./run.sh"],
|
||||||
|
cwd: tmp,
|
||||||
|
shellCommand: null,
|
||||||
|
});
|
||||||
|
expect(snapshot).toEqual({
|
||||||
|
ok: true,
|
||||||
|
snapshot: {
|
||||||
|
argvIndex: 3,
|
||||||
|
path: fs.realpathSync(scriptPath),
|
||||||
|
sha256: expect.any(String),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(tmp, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
resolveInlineCommandMatch,
|
resolveInlineCommandMatch,
|
||||||
} from "../infra/shell-inline-command.js";
|
} from "../infra/shell-inline-command.js";
|
||||||
import { formatExecCommand, resolveSystemRunCommandRequest } from "../infra/system-run-command.js";
|
import { formatExecCommand, resolveSystemRunCommandRequest } from "../infra/system-run-command.js";
|
||||||
|
import { splitShellArgs } from "../utils/shell-argv.js";
|
||||||
|
|
||||||
export type ApprovedCwdSnapshot = {
|
export type ApprovedCwdSnapshot = {
|
||||||
cwd: string;
|
cwd: string;
|
||||||
@@ -125,6 +126,47 @@ const DENO_RUN_OPTIONS_WITH_VALUE = new Set([
|
|||||||
"-L",
|
"-L",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const NODE_OPTIONS_WITH_FILE_VALUE = new Set([
|
||||||
|
"-r",
|
||||||
|
"--experimental-loader",
|
||||||
|
"--import",
|
||||||
|
"--loader",
|
||||||
|
"--require",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const POSIX_SHELL_OPTIONS_WITH_VALUE = new Set([
|
||||||
|
"--init-file",
|
||||||
|
"--rcfile",
|
||||||
|
"--startup-script",
|
||||||
|
"-o",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const NPM_EXEC_OPTIONS_WITH_VALUE = new Set([
|
||||||
|
"--cache",
|
||||||
|
"--package",
|
||||||
|
"--prefix",
|
||||||
|
"--script-shell",
|
||||||
|
"--userconfig",
|
||||||
|
"--workspace",
|
||||||
|
"-p",
|
||||||
|
"-w",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const NPM_EXEC_FLAG_OPTIONS = new Set([
|
||||||
|
"--no",
|
||||||
|
"--quiet",
|
||||||
|
"--ws",
|
||||||
|
"--workspaces",
|
||||||
|
"--yes",
|
||||||
|
"-q",
|
||||||
|
"-y",
|
||||||
|
]);
|
||||||
|
|
||||||
|
type FileOperandCollection = {
|
||||||
|
hits: number[];
|
||||||
|
sawOptionValueFile: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
function normalizeString(value: unknown): string | null {
|
function normalizeString(value: unknown): string | null {
|
||||||
if (typeof value !== "string") {
|
if (typeof value !== "string") {
|
||||||
return null;
|
return null;
|
||||||
@@ -225,10 +267,129 @@ function unwrapArgvForMutableOperand(argv: string[]): { argv: string[]; baseInde
|
|||||||
current = shellMultiplexerUnwrap.argv;
|
current = shellMultiplexerUnwrap.argv;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const packageManagerUnwrap = unwrapKnownPackageManagerExecInvocation(current);
|
||||||
|
if (packageManagerUnwrap) {
|
||||||
|
baseIndex += current.length - packageManagerUnwrap.length;
|
||||||
|
current = packageManagerUnwrap;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
return { argv: current, baseIndex };
|
return { argv: current, baseIndex };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function unwrapKnownPackageManagerExecInvocation(argv: string[]): string[] | null {
|
||||||
|
const executable = normalizePackageManagerExecToken(argv[0] ?? "");
|
||||||
|
switch (executable) {
|
||||||
|
case "npm":
|
||||||
|
return unwrapNpmExecInvocation(argv);
|
||||||
|
case "npx":
|
||||||
|
case "bunx":
|
||||||
|
return unwrapDirectPackageExecInvocation(argv);
|
||||||
|
case "pnpm":
|
||||||
|
return unwrapPnpmExecInvocation(argv);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePackageManagerExecToken(token: string): string {
|
||||||
|
const normalized = normalizeExecutableToken(token);
|
||||||
|
if (!normalized) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
return normalized.replace(/\.(?:c|m)?js$/i, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function unwrapPnpmExecInvocation(argv: string[]): string[] | null {
|
||||||
|
let idx = 1;
|
||||||
|
while (idx < argv.length) {
|
||||||
|
const token = argv[idx]?.trim() ?? "";
|
||||||
|
if (!token) {
|
||||||
|
idx += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (token === "--") {
|
||||||
|
idx += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!token.startsWith("-")) {
|
||||||
|
if (token !== "exec" || idx + 1 >= argv.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const tail = argv.slice(idx + 1);
|
||||||
|
return tail[0] === "--" ? (tail.length > 1 ? tail.slice(1) : null) : tail;
|
||||||
|
}
|
||||||
|
if ((token === "-C" || token === "--dir" || token === "--filter") && !token.includes("=")) {
|
||||||
|
idx += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function unwrapDirectPackageExecInvocation(argv: string[]): string[] | null {
|
||||||
|
let idx = 1;
|
||||||
|
while (idx < argv.length) {
|
||||||
|
const token = argv[idx]?.trim() ?? "";
|
||||||
|
if (!token) {
|
||||||
|
idx += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!token.startsWith("-")) {
|
||||||
|
return argv.slice(idx);
|
||||||
|
}
|
||||||
|
const [flag] = token.toLowerCase().split("=", 2);
|
||||||
|
if (flag === "-c" || flag === "--call") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (NPM_EXEC_OPTIONS_WITH_VALUE.has(flag)) {
|
||||||
|
idx += token.includes("=") ? 1 : 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (NPM_EXEC_FLAG_OPTIONS.has(flag)) {
|
||||||
|
idx += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function unwrapNpmExecInvocation(argv: string[]): string[] | null {
|
||||||
|
let idx = 1;
|
||||||
|
while (idx < argv.length) {
|
||||||
|
const token = argv[idx]?.trim() ?? "";
|
||||||
|
if (!token) {
|
||||||
|
idx += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!token.startsWith("-")) {
|
||||||
|
if (token !== "exec") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
idx += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
(token === "-C" || token === "--prefix" || token === "--userconfig") &&
|
||||||
|
!token.includes("=")
|
||||||
|
) {
|
||||||
|
idx += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
if (idx >= argv.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const tail = argv.slice(idx);
|
||||||
|
if (tail[0] === "--") {
|
||||||
|
return tail.length > 1 ? tail.slice(1) : null;
|
||||||
|
}
|
||||||
|
return unwrapDirectPackageExecInvocation(["npx", ...tail]);
|
||||||
|
}
|
||||||
|
|
||||||
function resolvePosixShellScriptOperandIndex(argv: string[]): number | null {
|
function resolvePosixShellScriptOperandIndex(argv: string[]): number | null {
|
||||||
if (
|
if (
|
||||||
resolveInlineCommandMatch(argv, POSIX_INLINE_COMMAND_FLAGS, {
|
resolveInlineCommandMatch(argv, POSIX_INLINE_COMMAND_FLAGS, {
|
||||||
@@ -254,6 +415,13 @@ function resolvePosixShellScriptOperandIndex(argv: string[]): number | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (!afterDoubleDash && token.startsWith("-")) {
|
if (!afterDoubleDash && token.startsWith("-")) {
|
||||||
|
const [flag] = token.toLowerCase().split("=", 2);
|
||||||
|
if (POSIX_SHELL_OPTIONS_WITH_VALUE.has(flag)) {
|
||||||
|
if (!token.includes("=")) {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
return i;
|
return i;
|
||||||
@@ -330,7 +498,8 @@ function collectExistingFileOperandIndexes(params: {
|
|||||||
argv: string[];
|
argv: string[];
|
||||||
startIndex: number;
|
startIndex: number;
|
||||||
cwd: string | undefined;
|
cwd: string | undefined;
|
||||||
}): number[] {
|
optionsWithFileValue?: ReadonlySet<string>;
|
||||||
|
}): FileOperandCollection {
|
||||||
let afterDoubleDash = false;
|
let afterDoubleDash = false;
|
||||||
const hits: number[] = [];
|
const hits: number[] = [];
|
||||||
for (let i = params.startIndex; i < params.argv.length; i += 1) {
|
for (let i = params.startIndex; i < params.argv.length; i += 1) {
|
||||||
@@ -349,28 +518,45 @@ function collectExistingFileOperandIndexes(params: {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (token === "-") {
|
if (token === "-") {
|
||||||
return [];
|
return { hits: [], sawOptionValueFile: false };
|
||||||
}
|
}
|
||||||
if (token.startsWith("-")) {
|
if (token.startsWith("-")) {
|
||||||
|
const [flag, inlineValue] = token.split("=", 2);
|
||||||
|
if (params.optionsWithFileValue?.has(flag.toLowerCase())) {
|
||||||
|
if (inlineValue && resolvesToExistingFileSync(inlineValue, params.cwd)) {
|
||||||
|
hits.push(i);
|
||||||
|
return { hits, sawOptionValueFile: true };
|
||||||
|
}
|
||||||
|
const nextToken = params.argv[i + 1]?.trim() ?? "";
|
||||||
|
if (!inlineValue && nextToken && resolvesToExistingFileSync(nextToken, params.cwd)) {
|
||||||
|
hits.push(i + 1);
|
||||||
|
return { hits, sawOptionValueFile: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (resolvesToExistingFileSync(token, params.cwd)) {
|
if (resolvesToExistingFileSync(token, params.cwd)) {
|
||||||
hits.push(i);
|
hits.push(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return hits;
|
return { hits, sawOptionValueFile: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveGenericInterpreterScriptOperandIndex(params: {
|
function resolveGenericInterpreterScriptOperandIndex(params: {
|
||||||
argv: string[];
|
argv: string[];
|
||||||
cwd: string | undefined;
|
cwd: string | undefined;
|
||||||
|
optionsWithFileValue?: ReadonlySet<string>;
|
||||||
}): number | null {
|
}): number | null {
|
||||||
const hits = collectExistingFileOperandIndexes({
|
const collection = collectExistingFileOperandIndexes({
|
||||||
argv: params.argv,
|
argv: params.argv,
|
||||||
startIndex: 1,
|
startIndex: 1,
|
||||||
cwd: params.cwd,
|
cwd: params.cwd,
|
||||||
|
optionsWithFileValue: params.optionsWithFileValue,
|
||||||
});
|
});
|
||||||
return hits.length === 1 ? hits[0] : null;
|
if (collection.sawOptionValueFile) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return collection.hits.length === 1 ? collection.hits[0] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveBunScriptOperandIndex(params: {
|
function resolveBunScriptOperandIndex(params: {
|
||||||
@@ -462,16 +648,39 @@ function resolveMutableFileOperandIndex(argv: string[], cwd: string | undefined)
|
|||||||
const genericIndex = resolveGenericInterpreterScriptOperandIndex({
|
const genericIndex = resolveGenericInterpreterScriptOperandIndex({
|
||||||
argv: unwrapped.argv,
|
argv: unwrapped.argv,
|
||||||
cwd,
|
cwd,
|
||||||
|
optionsWithFileValue:
|
||||||
|
executable === "node" || executable === "nodejs" ? NODE_OPTIONS_WITH_FILE_VALUE : undefined,
|
||||||
});
|
});
|
||||||
return genericIndex === null ? null : unwrapped.baseIndex + genericIndex;
|
return genericIndex === null ? null : unwrapped.baseIndex + genericIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shellPayloadNeedsStableBinding(shellCommand: string, cwd: string | undefined): boolean {
|
||||||
|
const argv = splitShellArgs(shellCommand);
|
||||||
|
if (!argv || argv.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const snapshot = resolveMutableFileOperandSnapshotSync({
|
||||||
|
argv,
|
||||||
|
cwd,
|
||||||
|
shellCommand: null,
|
||||||
|
});
|
||||||
|
if (!snapshot.ok) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (snapshot.snapshot) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const firstToken = argv[0]?.trim() ?? "";
|
||||||
|
return resolvesToExistingFileSync(firstToken, cwd);
|
||||||
|
}
|
||||||
|
|
||||||
function requiresStableInterpreterApprovalBindingWithShellCommand(params: {
|
function requiresStableInterpreterApprovalBindingWithShellCommand(params: {
|
||||||
argv: string[];
|
argv: string[];
|
||||||
shellCommand: string | null;
|
shellCommand: string | null;
|
||||||
|
cwd: string | undefined;
|
||||||
}): boolean {
|
}): boolean {
|
||||||
if (params.shellCommand !== null) {
|
if (params.shellCommand !== null) {
|
||||||
return false;
|
return shellPayloadNeedsStableBinding(params.shellCommand, params.cwd);
|
||||||
}
|
}
|
||||||
const unwrapped = unwrapArgvForMutableOperand(params.argv);
|
const unwrapped = unwrapArgvForMutableOperand(params.argv);
|
||||||
const executable = normalizeExecutableToken(unwrapped.argv[0] ?? "");
|
const executable = normalizeExecutableToken(unwrapped.argv[0] ?? "");
|
||||||
@@ -495,6 +704,7 @@ export function resolveMutableFileOperandSnapshotSync(params: {
|
|||||||
requiresStableInterpreterApprovalBindingWithShellCommand({
|
requiresStableInterpreterApprovalBindingWithShellCommand({
|
||||||
argv: params.argv,
|
argv: params.argv,
|
||||||
shellCommand: params.shellCommand,
|
shellCommand: params.shellCommand,
|
||||||
|
cwd: params.cwd,
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user