mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-31 07:56:53 +00:00
fix(security): harden node exec approvals against symlink rebind
This commit is contained in:
@@ -16,6 +16,7 @@ import {
|
||||
type ExecAsk,
|
||||
type ExecCommandSegment,
|
||||
type ExecSecurity,
|
||||
type SystemRunApprovalPlanV2,
|
||||
type SkillBinTrustEntry,
|
||||
} from "../infra/exec-approvals.js";
|
||||
import type { ExecHostRequest, ExecHostResponse, ExecHostRunResult } from "../infra/exec-host.js";
|
||||
@@ -113,6 +114,14 @@ function normalizeDeniedReason(reason: string | null | undefined): SystemRunDeni
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeString(value: unknown): string | null {
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed ? trimmed : null;
|
||||
}
|
||||
|
||||
function isPathLikeExecutableToken(value: string): boolean {
|
||||
if (!value) {
|
||||
return false;
|
||||
@@ -129,6 +138,46 @@ function isPathLikeExecutableToken(value: string): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
function pathComponentsFromRootSync(targetPath: string): string[] {
|
||||
const absolute = path.resolve(targetPath);
|
||||
const parts: string[] = [];
|
||||
let cursor = absolute;
|
||||
while (true) {
|
||||
parts.unshift(cursor);
|
||||
const parent = path.dirname(cursor);
|
||||
if (parent === cursor) {
|
||||
return parts;
|
||||
}
|
||||
cursor = parent;
|
||||
}
|
||||
}
|
||||
|
||||
function isWritableByCurrentProcessSync(candidate: string): boolean {
|
||||
try {
|
||||
fs.accessSync(candidate, fs.constants.W_OK);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function hasMutableSymlinkPathComponentSync(targetPath: string): boolean {
|
||||
for (const component of pathComponentsFromRootSync(targetPath)) {
|
||||
try {
|
||||
if (!fs.lstatSync(component).isSymbolicLink()) {
|
||||
continue;
|
||||
}
|
||||
const parentDir = path.dirname(component);
|
||||
if (isWritableByCurrentProcessSync(parentDir)) {
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function hardenApprovedExecutionPaths(params: {
|
||||
approvedByAsk: boolean;
|
||||
argv: string[];
|
||||
@@ -163,6 +212,12 @@ function hardenApprovedExecutionPaths(params: {
|
||||
message: "SYSTEM_RUN_DENIED: approval requires cwd to be a directory",
|
||||
};
|
||||
}
|
||||
if (hasMutableSymlinkPathComponentSync(requestedCwd)) {
|
||||
return {
|
||||
ok: false,
|
||||
message: "SYSTEM_RUN_DENIED: approval requires canonical cwd (no symlink path components)",
|
||||
};
|
||||
}
|
||||
if (cwdLstat.isSymbolicLink()) {
|
||||
return {
|
||||
ok: false,
|
||||
@@ -207,6 +262,46 @@ function hardenApprovedExecutionPaths(params: {
|
||||
return { ok: true, argv, cwd: hardenedCwd };
|
||||
}
|
||||
|
||||
export function buildSystemRunApprovalPlanV2(params: {
|
||||
command?: unknown;
|
||||
rawCommand?: unknown;
|
||||
cwd?: unknown;
|
||||
agentId?: unknown;
|
||||
sessionKey?: unknown;
|
||||
}): { ok: true; plan: SystemRunApprovalPlanV2; cmdText: string } | { ok: false; message: string } {
|
||||
const command = resolveSystemRunCommand({
|
||||
command: params.command,
|
||||
rawCommand: params.rawCommand,
|
||||
});
|
||||
if (!command.ok) {
|
||||
return { ok: false, message: command.message };
|
||||
}
|
||||
if (command.argv.length === 0) {
|
||||
return { ok: false, message: "command required" };
|
||||
}
|
||||
const hardening = hardenApprovedExecutionPaths({
|
||||
approvedByAsk: true,
|
||||
argv: command.argv,
|
||||
shellCommand: command.shellCommand,
|
||||
cwd: normalizeString(params.cwd) ?? undefined,
|
||||
});
|
||||
if (!hardening.ok) {
|
||||
return { ok: false, message: hardening.message };
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
plan: {
|
||||
version: 2,
|
||||
argv: hardening.argv,
|
||||
cwd: hardening.cwd ?? null,
|
||||
rawCommand: command.cmdText.trim() || null,
|
||||
agentId: normalizeString(params.agentId),
|
||||
sessionKey: normalizeString(params.sessionKey),
|
||||
},
|
||||
cmdText: command.cmdText,
|
||||
};
|
||||
}
|
||||
|
||||
export type HandleSystemRunInvokeOptions = {
|
||||
client: GatewayClient;
|
||||
params: SystemRunParams;
|
||||
|
||||
Reference in New Issue
Block a user