mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-07 22:09:57 +00:00
fix(node-host): bind approved script operands
This commit is contained in:
@@ -1,8 +1,22 @@
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { SystemRunApprovalPlan } from "../infra/exec-approvals.js";
|
||||
import type {
|
||||
SystemRunApprovalFileOperand,
|
||||
SystemRunApprovalPlan,
|
||||
} from "../infra/exec-approvals.js";
|
||||
import { resolveCommandResolutionFromArgv } from "../infra/exec-command-resolution.js";
|
||||
import {
|
||||
POSIX_SHELL_WRAPPERS,
|
||||
normalizeExecutableToken,
|
||||
unwrapKnownDispatchWrapperInvocation,
|
||||
unwrapKnownShellMultiplexerInvocation,
|
||||
} from "../infra/exec-wrapper-resolution.js";
|
||||
import { sameFileIdentity } from "../infra/file-identity.js";
|
||||
import {
|
||||
POSIX_INLINE_COMMAND_FLAGS,
|
||||
resolveInlineCommandMatch,
|
||||
} from "../infra/shell-inline-command.js";
|
||||
import { formatExecCommand, resolveSystemRunCommand } from "../infra/system-run-command.js";
|
||||
|
||||
export type ApprovedCwdSnapshot = {
|
||||
@@ -10,6 +24,14 @@ export type ApprovedCwdSnapshot = {
|
||||
stat: fs.Stats;
|
||||
};
|
||||
|
||||
const MUTABLE_ARGV1_INTERPRETER_PATTERNS = [
|
||||
/^(?:node|nodejs)$/,
|
||||
/^perl$/,
|
||||
/^php$/,
|
||||
/^python(?:\d+(?:\.\d+)*)?$/,
|
||||
/^ruby$/,
|
||||
] as const;
|
||||
|
||||
function normalizeString(value: unknown): string | null {
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
@@ -68,6 +90,125 @@ function shouldPinExecutableForApproval(params: {
|
||||
return (params.wrapperChain?.length ?? 0) === 0;
|
||||
}
|
||||
|
||||
function hashFileContentsSync(filePath: string): string {
|
||||
return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
|
||||
}
|
||||
|
||||
function unwrapArgvForMutableOperand(argv: string[]): { argv: string[]; baseIndex: number } {
|
||||
let current = argv;
|
||||
let baseIndex = 0;
|
||||
while (true) {
|
||||
const dispatchUnwrap = unwrapKnownDispatchWrapperInvocation(current);
|
||||
if (dispatchUnwrap.kind === "unwrapped") {
|
||||
baseIndex += current.length - dispatchUnwrap.argv.length;
|
||||
current = dispatchUnwrap.argv;
|
||||
continue;
|
||||
}
|
||||
const shellMultiplexerUnwrap = unwrapKnownShellMultiplexerInvocation(current);
|
||||
if (shellMultiplexerUnwrap.kind === "unwrapped") {
|
||||
baseIndex += current.length - shellMultiplexerUnwrap.argv.length;
|
||||
current = shellMultiplexerUnwrap.argv;
|
||||
continue;
|
||||
}
|
||||
return { argv: current, baseIndex };
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePosixShellScriptOperandIndex(argv: string[]): number | null {
|
||||
if (
|
||||
resolveInlineCommandMatch(argv, POSIX_INLINE_COMMAND_FLAGS, {
|
||||
allowCombinedC: true,
|
||||
}).valueTokenIndex !== null
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
let afterDoubleDash = false;
|
||||
for (let i = 1; i < argv.length; i += 1) {
|
||||
const token = argv[i]?.trim() ?? "";
|
||||
if (!token) {
|
||||
continue;
|
||||
}
|
||||
if (token === "-") {
|
||||
return null;
|
||||
}
|
||||
if (!afterDoubleDash && token === "--") {
|
||||
afterDoubleDash = true;
|
||||
continue;
|
||||
}
|
||||
if (!afterDoubleDash && token === "-s") {
|
||||
return null;
|
||||
}
|
||||
if (!afterDoubleDash && token.startsWith("-")) {
|
||||
continue;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveMutableFileOperandIndex(argv: string[]): number | null {
|
||||
const unwrapped = unwrapArgvForMutableOperand(argv);
|
||||
const executable = normalizeExecutableToken(unwrapped.argv[0] ?? "");
|
||||
if (!executable) {
|
||||
return null;
|
||||
}
|
||||
if ((POSIX_SHELL_WRAPPERS as ReadonlySet<string>).has(executable)) {
|
||||
const shellIndex = resolvePosixShellScriptOperandIndex(unwrapped.argv);
|
||||
return shellIndex === null ? null : unwrapped.baseIndex + shellIndex;
|
||||
}
|
||||
if (!MUTABLE_ARGV1_INTERPRETER_PATTERNS.some((pattern) => pattern.test(executable))) {
|
||||
return null;
|
||||
}
|
||||
const operand = unwrapped.argv[1]?.trim() ?? "";
|
||||
if (!operand || operand === "-" || operand.startsWith("-")) {
|
||||
return null;
|
||||
}
|
||||
return unwrapped.baseIndex + 1;
|
||||
}
|
||||
|
||||
function resolveMutableFileOperandSnapshotSync(params: {
|
||||
argv: string[];
|
||||
cwd: string | undefined;
|
||||
}): { ok: true; snapshot: SystemRunApprovalFileOperand | null } | { ok: false; message: string } {
|
||||
const argvIndex = resolveMutableFileOperandIndex(params.argv);
|
||||
if (argvIndex === null) {
|
||||
return { ok: true, snapshot: null };
|
||||
}
|
||||
const rawOperand = params.argv[argvIndex]?.trim();
|
||||
if (!rawOperand) {
|
||||
return {
|
||||
ok: false,
|
||||
message: "SYSTEM_RUN_DENIED: approval requires a stable script operand",
|
||||
};
|
||||
}
|
||||
const resolvedPath = path.resolve(params.cwd ?? process.cwd(), rawOperand);
|
||||
let realPath: string;
|
||||
let stat: fs.Stats;
|
||||
try {
|
||||
realPath = fs.realpathSync(resolvedPath);
|
||||
stat = fs.statSync(realPath);
|
||||
} catch {
|
||||
return {
|
||||
ok: false,
|
||||
message: "SYSTEM_RUN_DENIED: approval requires an existing script operand",
|
||||
};
|
||||
}
|
||||
if (!stat.isFile()) {
|
||||
return {
|
||||
ok: false,
|
||||
message: "SYSTEM_RUN_DENIED: approval requires a file script operand",
|
||||
};
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
snapshot: {
|
||||
argvIndex,
|
||||
path: realPath,
|
||||
sha256: hashFileContentsSync(realPath),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function resolveCanonicalApprovalCwdSync(cwd: string):
|
||||
| {
|
||||
ok: true;
|
||||
@@ -135,6 +276,32 @@ export function revalidateApprovedCwdSnapshot(params: { snapshot: ApprovedCwdSna
|
||||
return sameFileIdentity(params.snapshot.stat, current.snapshot.stat);
|
||||
}
|
||||
|
||||
export function revalidateApprovedMutableFileOperand(params: {
|
||||
snapshot: SystemRunApprovalFileOperand;
|
||||
argv: string[];
|
||||
cwd: string | undefined;
|
||||
}): boolean {
|
||||
const operand = params.argv[params.snapshot.argvIndex]?.trim();
|
||||
if (!operand) {
|
||||
return false;
|
||||
}
|
||||
const resolvedPath = path.resolve(params.cwd ?? process.cwd(), operand);
|
||||
let realPath: string;
|
||||
try {
|
||||
realPath = fs.realpathSync(resolvedPath);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
if (realPath !== params.snapshot.path) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return hashFileContentsSync(realPath) === params.snapshot.sha256;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function hardenApprovedExecutionPaths(params: {
|
||||
approvedByAsk: boolean;
|
||||
argv: string[];
|
||||
@@ -257,6 +424,13 @@ export function buildSystemRunApprovalPlan(params: {
|
||||
const rawCommand = hardening.argvChanged
|
||||
? formatExecCommand(hardening.argv) || null
|
||||
: command.cmdText.trim() || null;
|
||||
const mutableFileOperand = resolveMutableFileOperandSnapshotSync({
|
||||
argv: hardening.argv,
|
||||
cwd: hardening.cwd,
|
||||
});
|
||||
if (!mutableFileOperand.ok) {
|
||||
return { ok: false, message: mutableFileOperand.message };
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
plan: {
|
||||
@@ -265,7 +439,8 @@ export function buildSystemRunApprovalPlan(params: {
|
||||
rawCommand,
|
||||
agentId: normalizeString(params.agentId),
|
||||
sessionKey: normalizeString(params.sessionKey),
|
||||
mutableFileOperand: mutableFileOperand.snapshot ?? undefined,
|
||||
},
|
||||
cmdText: command.cmdText,
|
||||
cmdText: rawCommand ?? formatExecCommand(hardening.argv),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user