mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-25 00:42:24 +00:00
fix(security): harden exec approval boundaries
This commit is contained in:
@@ -9,6 +9,10 @@ import {
|
||||
requiresExecApproval,
|
||||
resolveAllowAlwaysPatterns,
|
||||
} from "../infra/exec-approvals.js";
|
||||
import {
|
||||
describeInterpreterInlineEval,
|
||||
detectInterpreterInlineEvalArgv,
|
||||
} from "../infra/exec-inline-eval.js";
|
||||
import { detectCommandObfuscation } from "../infra/exec-obfuscation-detect.js";
|
||||
import type { SafeBinProfile } from "../infra/exec-safe-bin-policy.js";
|
||||
import { logInfo } from "../logger.js";
|
||||
@@ -48,6 +52,7 @@ export type ProcessGatewayAllowlistParams = {
|
||||
ask: ExecAsk;
|
||||
safeBins: Set<string>;
|
||||
safeBinProfiles: Readonly<Record<string, SafeBinProfile>>;
|
||||
strictInlineEval?: boolean;
|
||||
agentId?: string;
|
||||
sessionKey?: string;
|
||||
turnSourceChannel?: string;
|
||||
@@ -91,6 +96,21 @@ export async function processGatewayAllowlist(
|
||||
const analysisOk = allowlistEval.analysisOk;
|
||||
const allowlistSatisfied =
|
||||
hostSecurity === "allowlist" && analysisOk ? allowlistEval.allowlistSatisfied : false;
|
||||
const inlineEvalHit =
|
||||
params.strictInlineEval === true
|
||||
? (allowlistEval.segments
|
||||
.map((segment) =>
|
||||
detectInterpreterInlineEvalArgv(segment.resolution?.effectiveArgv ?? segment.argv),
|
||||
)
|
||||
.find((entry) => entry !== null) ?? null)
|
||||
: null;
|
||||
if (inlineEvalHit) {
|
||||
params.warnings.push(
|
||||
`Warning: strict inline-eval mode requires explicit approval for ${describeInterpreterInlineEval(
|
||||
inlineEvalHit,
|
||||
)}.`,
|
||||
);
|
||||
}
|
||||
let enforcedCommand: string | undefined;
|
||||
if (hostSecurity === "allowlist" && analysisOk && allowlistSatisfied) {
|
||||
const enforced = buildEnforcedShellCommand({
|
||||
@@ -126,6 +146,7 @@ export async function processGatewayAllowlist(
|
||||
);
|
||||
const requiresHeredocApproval =
|
||||
hostSecurity === "allowlist" && analysisOk && allowlistSatisfied && hasHeredocSegment;
|
||||
const requiresInlineEvalApproval = inlineEvalHit !== null;
|
||||
const requiresAsk =
|
||||
requiresExecApproval({
|
||||
ask: hostAsk,
|
||||
@@ -134,6 +155,7 @@ export async function processGatewayAllowlist(
|
||||
allowlistSatisfied,
|
||||
}) ||
|
||||
requiresHeredocApproval ||
|
||||
requiresInlineEvalApproval ||
|
||||
obfuscation.detected;
|
||||
if (requiresHeredocApproval) {
|
||||
params.warnings.push(
|
||||
@@ -226,7 +248,7 @@ export async function processGatewayAllowlist(
|
||||
approvedByAsk = true;
|
||||
} else if (decision === "allow-always") {
|
||||
approvedByAsk = true;
|
||||
if (hostSecurity === "allowlist") {
|
||||
if (hostSecurity === "allowlist" && !requiresInlineEvalApproval) {
|
||||
const patterns = resolveAllowAlwaysPatterns({
|
||||
segments: allowlistEval.segments,
|
||||
cwd: params.workdir,
|
||||
|
||||
@@ -8,6 +8,10 @@ import {
|
||||
requiresExecApproval,
|
||||
resolveExecApprovalsFromFile,
|
||||
} from "../infra/exec-approvals.js";
|
||||
import {
|
||||
describeInterpreterInlineEval,
|
||||
detectInterpreterInlineEvalArgv,
|
||||
} from "../infra/exec-inline-eval.js";
|
||||
import { detectCommandObfuscation } from "../infra/exec-obfuscation-detect.js";
|
||||
import { buildNodeShellCommand } from "../infra/node-shell.js";
|
||||
import { parsePreparedSystemRunPayload } from "../infra/system-run-approval-context.js";
|
||||
@@ -42,6 +46,7 @@ export type ExecuteNodeHostCommandParams = {
|
||||
agentId?: string;
|
||||
security: ExecSecurity;
|
||||
ask: ExecAsk;
|
||||
strictInlineEval?: boolean;
|
||||
timeoutSec?: number;
|
||||
defaultTimeoutSec: number;
|
||||
approvalRunningNoticeMs: number;
|
||||
@@ -129,6 +134,21 @@ export async function executeNodeHostCommand(
|
||||
});
|
||||
let analysisOk = baseAllowlistEval.analysisOk;
|
||||
let allowlistSatisfied = false;
|
||||
const inlineEvalHit =
|
||||
params.strictInlineEval === true
|
||||
? (baseAllowlistEval.segments
|
||||
.map((segment) =>
|
||||
detectInterpreterInlineEvalArgv(segment.resolution?.effectiveArgv ?? segment.argv),
|
||||
)
|
||||
.find((entry) => entry !== null) ?? null)
|
||||
: null;
|
||||
if (inlineEvalHit) {
|
||||
params.warnings.push(
|
||||
`Warning: strict inline-eval mode requires explicit approval for ${describeInterpreterInlineEval(
|
||||
inlineEvalHit,
|
||||
)}.`,
|
||||
);
|
||||
}
|
||||
if (hostAsk === "on-miss" && hostSecurity === "allowlist" && analysisOk) {
|
||||
try {
|
||||
const approvalsSnapshot = await callGatewayTool<{ file: string }>(
|
||||
@@ -176,7 +196,9 @@ export async function executeNodeHostCommand(
|
||||
security: hostSecurity,
|
||||
analysisOk,
|
||||
allowlistSatisfied,
|
||||
}) || obfuscation.detected;
|
||||
}) ||
|
||||
inlineEvalHit !== null ||
|
||||
obfuscation.detected;
|
||||
const invokeTimeoutMs = Math.max(
|
||||
10_000,
|
||||
(typeof params.timeoutSec === "number" ? params.timeoutSec : params.defaultTimeoutSec) * 1000 +
|
||||
@@ -200,7 +222,10 @@ export async function executeNodeHostCommand(
|
||||
agentId: runAgentId,
|
||||
sessionKey: runSessionKey,
|
||||
approved: approvedByAsk,
|
||||
approvalDecision: approvalDecision ?? undefined,
|
||||
approvalDecision:
|
||||
approvalDecision === "allow-always" && inlineEvalHit !== null
|
||||
? "allow-once"
|
||||
: (approvalDecision ?? undefined),
|
||||
runId: runId ?? undefined,
|
||||
suppressNotifyOnExit: suppressNotifyOnExit === true ? true : undefined,
|
||||
},
|
||||
|
||||
@@ -9,6 +9,7 @@ export type ExecToolDefaults = {
|
||||
node?: string;
|
||||
pathPrepend?: string[];
|
||||
safeBins?: string[];
|
||||
strictInlineEval?: boolean;
|
||||
safeBinTrustedDirs?: string[];
|
||||
safeBinProfiles?: Record<string, SafeBinProfileFixture>;
|
||||
agentId?: string;
|
||||
|
||||
@@ -448,6 +448,7 @@ export function createExecTool(
|
||||
agentId,
|
||||
security,
|
||||
ask,
|
||||
strictInlineEval: defaults?.strictInlineEval,
|
||||
timeoutSec: params.timeout,
|
||||
defaultTimeoutSec,
|
||||
approvalRunningNoticeMs,
|
||||
@@ -470,6 +471,7 @@ export function createExecTool(
|
||||
ask,
|
||||
safeBins,
|
||||
safeBinProfiles,
|
||||
strictInlineEval: defaults?.strictInlineEval,
|
||||
agentId,
|
||||
sessionKey: defaults?.sessionKey,
|
||||
turnSourceChannel: defaults?.messageProvider,
|
||||
|
||||
@@ -143,6 +143,7 @@ function resolveExecConfig(params: { cfg?: OpenClawConfig; agentId?: string }) {
|
||||
node: agentExec?.node ?? globalExec?.node,
|
||||
pathPrepend: agentExec?.pathPrepend ?? globalExec?.pathPrepend,
|
||||
safeBins: agentExec?.safeBins ?? globalExec?.safeBins,
|
||||
strictInlineEval: agentExec?.strictInlineEval ?? globalExec?.strictInlineEval,
|
||||
safeBinTrustedDirs: agentExec?.safeBinTrustedDirs ?? globalExec?.safeBinTrustedDirs,
|
||||
safeBinProfiles: resolveMergedSafeBinProfileFixtures({
|
||||
global: globalExec,
|
||||
@@ -420,6 +421,7 @@ export function createOpenClawCodingTools(options?: {
|
||||
node: options?.exec?.node ?? execConfig.node,
|
||||
pathPrepend: options?.exec?.pathPrepend ?? execConfig.pathPrepend,
|
||||
safeBins: options?.exec?.safeBins ?? execConfig.safeBins,
|
||||
strictInlineEval: options?.exec?.strictInlineEval ?? execConfig.strictInlineEval,
|
||||
safeBinTrustedDirs: options?.exec?.safeBinTrustedDirs ?? execConfig.safeBinTrustedDirs,
|
||||
safeBinProfiles: options?.exec?.safeBinProfiles ?? execConfig.safeBinProfiles,
|
||||
agentId,
|
||||
|
||||
Reference in New Issue
Block a user