mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-23 07:01:40 +00:00
fix(exec): implement Windows argPattern allowlist flow
This commit is contained in:
@@ -246,9 +246,85 @@ export function resolvePolicyAllowlistCandidatePath(
|
||||
return resolvePolicyTargetCandidatePath(resolution, cwd);
|
||||
}
|
||||
|
||||
// Strip trailing shell redirections (e.g. `2>&1`, `2>/dev/null`) so that
|
||||
// allow-always argPatterns built without them still match commands that include
|
||||
// them. LLMs commonly add or omit these between runs of the same cron job.
|
||||
const TRAILING_SHELL_REDIRECTIONS_RE = /\s+(?:[12]>&[12]|[12]>\/dev\/null)\s*$/;
|
||||
|
||||
function stripTrailingRedirections(value: string): string {
|
||||
let prev = value;
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
const next = prev.replace(TRAILING_SHELL_REDIRECTIONS_RE, "");
|
||||
if (next === prev) {
|
||||
return next;
|
||||
}
|
||||
prev = next;
|
||||
}
|
||||
}
|
||||
|
||||
function matchArgPattern(argPattern: string, argv: string[], platform?: string | null): boolean {
|
||||
// Patterns built by buildArgPatternFromArgv use \x00 as the argument separator and
|
||||
// always include a trailing \x00 sentinel so that every auto-generated pattern
|
||||
// (including zero-arg "^\x00\x00$" and single-arg "^hello world\x00$") contains at
|
||||
// least one \x00. This lets matchArgPattern detect the join style unambiguously
|
||||
// via .includes("\x00") without misidentifying anchored hand-authored patterns.
|
||||
// Legacy hand-authored patterns use a plain space and contain no \x00.
|
||||
// When \x00 style is active, a trailing \x00 is appended to the joined args string
|
||||
// to match the sentinel embedded in the pattern.
|
||||
//
|
||||
// Zero args use a double sentinel "\x00\x00" to distinguish [] from [""] — both
|
||||
// join to "" but must match different patterns ("^\x00\x00$" vs "^\x00$").
|
||||
const sep = argPattern.includes("\x00") ? "\x00" : " ";
|
||||
const argsSlice = argv.slice(1);
|
||||
const argsString =
|
||||
sep === "\x00"
|
||||
? argsSlice.length === 0
|
||||
? "\x00\x00" // zero args: double sentinel matches "^\x00\x00$" pattern
|
||||
: argsSlice.join(sep) + sep // trailing sentinel to match pattern format
|
||||
: argsSlice.join(sep);
|
||||
try {
|
||||
const regex = new RegExp(argPattern);
|
||||
if (regex.test(argsString)) {
|
||||
return true;
|
||||
}
|
||||
// On Windows, LLMs may use forward slashes (`C:/path`) or backslashes
|
||||
// (`C:\path`) interchangeably. Normalize to backslashes and retry so
|
||||
// that an argPattern built from one style still matches the other.
|
||||
// Use the caller-supplied target platform so Linux gateways evaluating
|
||||
// Windows node commands also perform the normalization.
|
||||
const effectivePlatform = String(platform ?? process.platform)
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (effectivePlatform.startsWith("win")) {
|
||||
const normalized = argsString.replace(/\//g, "\\");
|
||||
if (normalized !== argsString && regex.test(normalized)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Retry after stripping trailing shell redirections (2>&1, etc.) so that
|
||||
// patterns saved without them still match commands that include them.
|
||||
// Only applies for space-joined (legacy hand-authored) patterns. For
|
||||
// \x00-joined auto-generated patterns, redirections are already blocked
|
||||
// upstream by findWindowsUnsupportedToken, so any surviving 2>&1 token
|
||||
// is a literal data argument and must not be stripped.
|
||||
if (sep === " ") {
|
||||
const stripped = stripTrailingRedirections(argsString);
|
||||
if (stripped !== argsString && regex.test(stripped)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function matchAllowlist(
|
||||
entries: ExecAllowlistEntry[],
|
||||
resolution: ExecutableResolution | null,
|
||||
argv?: string[],
|
||||
platform?: string | null,
|
||||
): ExecAllowlistEntry | null {
|
||||
if (!entries.length) {
|
||||
return null;
|
||||
@@ -256,7 +332,7 @@ export function matchAllowlist(
|
||||
// A bare "*" wildcard allows any parsed executable command.
|
||||
// Check it before the resolvedPath guard so unresolved PATH lookups still
|
||||
// match (for example platform-specific executables without known extensions).
|
||||
const bareWild = entries.find((e) => e.pattern?.trim() === "*");
|
||||
const bareWild = entries.find((e) => e.pattern?.trim() === "*" && !e.argPattern);
|
||||
if (bareWild && resolution) {
|
||||
return bareWild;
|
||||
}
|
||||
@@ -264,6 +340,14 @@ export function matchAllowlist(
|
||||
return null;
|
||||
}
|
||||
const resolvedPath = resolution.resolvedPath;
|
||||
// argPattern matching is currently Windows-only. On other platforms every
|
||||
// path-matched entry is treated as a match regardless of argPattern, which
|
||||
// preserves the pre-existing behaviour.
|
||||
// Use the caller-supplied target platform rather than process.platform so that
|
||||
// a Linux gateway evaluating a Windows node command applies argPattern correctly.
|
||||
const effectivePlatform = platform ?? process.platform;
|
||||
const useArgPattern = String(effectivePlatform).trim().toLowerCase().startsWith("win");
|
||||
let pathOnlyMatch: ExecAllowlistEntry | null = null;
|
||||
for (const entry of entries) {
|
||||
const pattern = entry.pattern?.trim();
|
||||
if (!pattern) {
|
||||
@@ -273,11 +357,25 @@ export function matchAllowlist(
|
||||
if (!hasPath) {
|
||||
continue;
|
||||
}
|
||||
if (matchesExecAllowlistPattern(pattern, resolvedPath)) {
|
||||
if (!matchesExecAllowlistPattern(pattern, resolvedPath)) {
|
||||
continue;
|
||||
}
|
||||
if (!useArgPattern) {
|
||||
// Non-Windows: first path match wins (legacy behaviour).
|
||||
return entry;
|
||||
}
|
||||
if (!entry.argPattern) {
|
||||
if (!pathOnlyMatch) {
|
||||
pathOnlyMatch = entry;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Entry has argPattern — check argv match.
|
||||
if (argv && matchArgPattern(entry.argPattern, argv, platform)) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return pathOnlyMatch;
|
||||
}
|
||||
|
||||
export type ExecArgvToken =
|
||||
|
||||
Reference in New Issue
Block a user