mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-14 10:41:23 +00:00
fix(agents): replace .* with \S* in interpreter heuristic regexes to prevent ReDoS
The inner `.*\s+` in `(?:[A-Za-z_][A-Za-z0-9_]*=.*\s+)*` creates catastrophic backtracking because both `.*` and `\s+` can match whitespace. When the exec tool processes commands with `VAR=value` assignments followed by whitespace-heavy text (e.g. HTML heredocs), the regex engine hangs permanently at 100% CPU. Replace `.*` with `\S*` in all three instances. Shell prefix variable assignments cannot contain unquoted whitespace in the value, so `\S*` is semantically correct and eliminates the ambiguity. Fixes #61881
This commit is contained in:
committed by
Peter Steinberger
parent
c63a4f0f13
commit
eede8f945f
@@ -783,3 +783,32 @@ describeWin("exec script preflight on windows path syntax", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("exec interpreter heuristics ReDoS guard", () => {
|
||||
it("does not hang on long commands with VAR=value assignments and whitespace-heavy text", async () => {
|
||||
const tool = createExecTool({ host: "gateway", security: "full", ask: "off" });
|
||||
// Simulate a heredoc with HTML content after a VAR= assignment — the pattern
|
||||
// that triggers catastrophic backtracking when .* is used instead of \S*
|
||||
const htmlBlock = '<section style="padding: 30px 20px; font-family: Arial;">'.repeat(50);
|
||||
const command = `ACCESS_TOKEN=$(curl -s https://api.example.com/token)\ncat > /tmp/out.html << 'EOF'\n${htmlBlock}\nEOF`;
|
||||
|
||||
const start = Date.now();
|
||||
// The command itself will fail — we only care that the interpreter
|
||||
// heuristics analysis completes without hanging.
|
||||
try {
|
||||
await Promise.race([
|
||||
tool.execute("redos-guard", { command }),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error("ReDoS: regex hung for >5s")), 5000),
|
||||
),
|
||||
]);
|
||||
} catch (e) {
|
||||
// Any error EXCEPT the timeout is acceptable — it means the regex finished
|
||||
if (e instanceof Error && e.message.includes("ReDoS")) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
const elapsed = Date.now() - start;
|
||||
expect(elapsed).toBeLessThan(5000);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -418,11 +418,11 @@ function analyzeInterpreterHeuristicsFromUnquoted(raw: string): {
|
||||
hasScriptHint: boolean;
|
||||
} {
|
||||
const hasPython =
|
||||
/(?:^|\s|(?<!\\)[|&;()])(?:[A-Za-z_][A-Za-z0-9_]*=.*\s+)*python(?:3(?:\.\d+)?)?(?=$|[\s|&;()<>\n\r`$])/i.test(
|
||||
/(?:^|\s|(?<!\\)[|&;()])(?:[A-Za-z_][A-Za-z0-9_]*=\S*\s+)*python(?:3(?:\.\d+)?)?(?=$|[\s|&;()<>\n\r`$])/i.test(
|
||||
raw,
|
||||
);
|
||||
const hasNode =
|
||||
/(?:^|\s|(?<!\\)[|&;()])(?:[A-Za-z_][A-Za-z0-9_]*=.*\s+)*node(?=$|[\s|&;()<>\n\r`$])/i.test(
|
||||
/(?:^|\s|(?<!\\)[|&;()])(?:[A-Za-z_][A-Za-z0-9_]*=\S*\s+)*node(?=$|[\s|&;()<>\n\r`$])/i.test(
|
||||
raw,
|
||||
);
|
||||
const hasProcessSubstitution = /(?<!\\)<\(|(?<!\\)>\(/u.test(raw);
|
||||
@@ -622,7 +622,7 @@ function shouldFailClosedInterpreterPreflight(command: string): {
|
||||
};
|
||||
const hasInterpreterInvocationInSegment = (rawSegment: string): boolean => {
|
||||
const segment = extractUnquotedShellText(rawSegment) ?? rawSegment;
|
||||
return /^\s*(?:(?:if|then|do|elif|else|while|until|time)\s+)?(?:[A-Za-z_][A-Za-z0-9_]*=.*\s+)*(?:python(?:3(?:\.\d+)?)?|node)(?=$|[\s|&;()<>\n\r`$])/i.test(
|
||||
return /^\s*(?:(?:if|then|do|elif|else|while|until|time)\s+)?(?:[A-Za-z_][A-Za-z0-9_]*=\S*\s+)*(?:python(?:3(?:\.\d+)?)?|node)(?=$|[\s|&;()<>\n\r`$])/i.test(
|
||||
segment,
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user