mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 03:11:25 +00:00
refactor(security): refine safeBins hardening
This commit is contained in:
@@ -772,109 +772,75 @@ export function buildSafeShellCommand(params: { command: string; platform?: stri
|
||||
return { ok: true, command: out };
|
||||
}
|
||||
|
||||
function renderQuotedArgv(argv: string[]): string {
|
||||
return argv.map((token) => shellEscapeSingleArg(token)).join(" ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuilds a shell command and selectively single-quotes argv tokens for segments that
|
||||
* must be treated as literal (safeBins hardening) while preserving the rest of the
|
||||
* shell syntax (pipes + chaining).
|
||||
*/
|
||||
export function buildSafeBinsShellCommand(params: {
|
||||
command: string;
|
||||
segments: ExecCommandSegment[];
|
||||
segmentSatisfiedBy: ("allowlist" | "safeBins" | "skills" | null)[];
|
||||
platform?: string | null;
|
||||
}): { ok: boolean; command?: string; reason?: string } {
|
||||
const platform = params.platform ?? null;
|
||||
if (isWindowsPlatform(platform)) {
|
||||
return { ok: false, reason: "unsupported platform" };
|
||||
}
|
||||
if (params.segments.length !== params.segmentSatisfiedBy.length) {
|
||||
return { ok: false, reason: "segment metadata mismatch" };
|
||||
}
|
||||
|
||||
const chain = splitCommandChainWithOperators(params.command.trim());
|
||||
const chainParts: ShellChainPart[] = chain ?? [{ part: params.command.trim(), opToNext: null }];
|
||||
let segIndex = 0;
|
||||
let out = "";
|
||||
|
||||
for (const part of chainParts) {
|
||||
const pipelineSplit = splitShellPipeline(part.part);
|
||||
if (!pipelineSplit.ok) {
|
||||
return { ok: false, reason: pipelineSplit.reason ?? "unable to parse pipeline" };
|
||||
}
|
||||
|
||||
const rendered: string[] = [];
|
||||
for (const raw of pipelineSplit.segments) {
|
||||
const seg = params.segments[segIndex];
|
||||
const by = params.segmentSatisfiedBy[segIndex];
|
||||
if (!seg || by === undefined) {
|
||||
return { ok: false, reason: "segment mapping failed" };
|
||||
}
|
||||
const needsLiteral = by === "safeBins";
|
||||
rendered.push(needsLiteral ? renderQuotedArgv(seg.argv) : raw.trim());
|
||||
segIndex += 1;
|
||||
}
|
||||
|
||||
out += rendered.join(" | ");
|
||||
if (part.opToNext) {
|
||||
out += ` ${part.opToNext} `;
|
||||
}
|
||||
}
|
||||
|
||||
if (segIndex !== params.segments.length) {
|
||||
return { ok: false, reason: "segment count mismatch" };
|
||||
}
|
||||
|
||||
return { ok: true, command: out };
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a command string by chain operators (&&, ||, ;) while respecting quotes.
|
||||
* Returns null when no chain is present or when the chain is malformed.
|
||||
*/
|
||||
export function splitCommandChain(command: string): string[] | null {
|
||||
const parts: string[] = [];
|
||||
let buf = "";
|
||||
let inSingle = false;
|
||||
let inDouble = false;
|
||||
let escaped = false;
|
||||
let foundChain = false;
|
||||
let invalidChain = false;
|
||||
|
||||
const pushPart = () => {
|
||||
const trimmed = buf.trim();
|
||||
if (trimmed) {
|
||||
parts.push(trimmed);
|
||||
buf = "";
|
||||
return true;
|
||||
}
|
||||
buf = "";
|
||||
return false;
|
||||
};
|
||||
|
||||
for (let i = 0; i < command.length; i += 1) {
|
||||
const ch = command[i];
|
||||
const next = command[i + 1];
|
||||
if (escaped) {
|
||||
buf += ch;
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
if (!inSingle && !inDouble && ch === "\\") {
|
||||
escaped = true;
|
||||
buf += ch;
|
||||
continue;
|
||||
}
|
||||
if (inSingle) {
|
||||
if (ch === "'") {
|
||||
inSingle = false;
|
||||
}
|
||||
buf += ch;
|
||||
continue;
|
||||
}
|
||||
if (inDouble) {
|
||||
if (ch === "\\" && isDoubleQuoteEscape(next)) {
|
||||
buf += ch;
|
||||
buf += next;
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (ch === '"') {
|
||||
inDouble = false;
|
||||
}
|
||||
buf += ch;
|
||||
continue;
|
||||
}
|
||||
if (ch === "'") {
|
||||
inSingle = true;
|
||||
buf += ch;
|
||||
continue;
|
||||
}
|
||||
if (ch === '"') {
|
||||
inDouble = true;
|
||||
buf += ch;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === "&" && command[i + 1] === "&") {
|
||||
if (!pushPart()) {
|
||||
invalidChain = true;
|
||||
}
|
||||
i += 1;
|
||||
foundChain = true;
|
||||
continue;
|
||||
}
|
||||
if (ch === "|" && command[i + 1] === "|") {
|
||||
if (!pushPart()) {
|
||||
invalidChain = true;
|
||||
}
|
||||
i += 1;
|
||||
foundChain = true;
|
||||
continue;
|
||||
}
|
||||
if (ch === ";") {
|
||||
if (!pushPart()) {
|
||||
invalidChain = true;
|
||||
}
|
||||
foundChain = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
buf += ch;
|
||||
}
|
||||
|
||||
const pushedFinal = pushPart();
|
||||
if (!foundChain) {
|
||||
const parts = splitCommandChainWithOperators(command);
|
||||
if (!parts) {
|
||||
return null;
|
||||
}
|
||||
if (invalidChain || !pushedFinal) {
|
||||
return null;
|
||||
}
|
||||
return parts.length > 0 ? parts : null;
|
||||
return parts.map((p) => p.part);
|
||||
}
|
||||
|
||||
export function analyzeShellCommand(params: {
|
||||
|
||||
Reference in New Issue
Block a user