fix(agents): prevent ReDoS in background-session name derivation (#91233)

Summary:
- The PR updates background-session command tokenization to avoid catastrophic regex backtracking and adds `deriveSessionName` regression tests for quoted and backslash-heavy commands.
- PR surface: Source 0, Tests +26. Total +26 across 2 files.
- Reproducibility: yes. with high confidence from source inspection and supplied terminal proof: current `main ...  shows before/after timing for the production helper. I did not run tests because this review is read-only.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(agents): treat backslash as literal inside single-quoted session …
- PR branch already contained follow-up commit before automerge: fix(agents): prevent ReDoS in background-session name derivation

Validation:
- ClawSweeper review passed for head 0a38952fc8.
- Required merge gates passed before the squash merge.

Prepared head SHA: 0a38952fc8
Review: https://github.com/openclaw/openclaw/pull/91233#issuecomment-4643821335

Co-authored-by: yetval <yetvald@gmail.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
This commit is contained in:
clawsweeper[bot]
2026-06-07 20:30:56 +00:00
committed by GitHub
co-authored by yetval clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> takhoffman
parent 0c33f4e078
commit e498d39bed
2 changed files with 28 additions and 2 deletions
+27 -1
View File
@@ -7,7 +7,7 @@ import { mkdir, mkdtemp, rm } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { readEnvInt, resolveSandboxWorkdir } from "./bash-tools.shared.js";
import { deriveSessionName, readEnvInt, resolveSandboxWorkdir } from "./bash-tools.shared.js";
async function withTempDir(run: (dir: string) => Promise<void>) {
const dir = await mkdtemp(path.join(os.tmpdir(), "openclaw-bash-workdir-"));
@@ -118,3 +118,29 @@ describe("resolveSandboxWorkdir", () => {
});
});
});
describe("deriveSessionName", () => {
it("labels well-formed quoted commands", () => {
expect(deriveSessionName('node "my server.js" --port 8080')).toBe("node my server.js");
expect(deriveSessionName("git commit -m 'fix bug'")).toBe("git commit");
});
it("keeps grouping backslash-bearing quoted spans into one token", () => {
expect(deriveSessionName('tar "a\\b c"')).toBe("tar a\\b c");
});
it("treats backslash as literal inside single-quoted spans", () => {
expect(deriveSessionName("cmd 'a b\\' next")).toBe("cmd a b\\");
});
it("returns a label without catastrophic backtracking on unterminated quoted backslash runs", () => {
for (const quote of [`"`, `'`]) {
const malicious = `node ${quote}${"\\".repeat(50000)}`;
const start = process.hrtime.bigint();
const label = deriveSessionName(malicious);
const elapsedMs = Number(process.hrtime.bigint() - start) / 1e6;
expect(typeof label).toBe("string");
expect(elapsedMs).toBeLessThan(100);
}
});
});
+1 -1
View File
@@ -291,7 +291,7 @@ export function deriveSessionName(command: string): string | undefined {
}
function tokenizeCommand(command: string): string[] {
const matches = command.match(/(?:[^\s"']+|"(?:\\.|[^"])*"|'(?:\\.|[^'])*')+/g) ?? [];
const matches = command.match(/(?:[^\s"']+|"(?:\\.|[^"\\])*"|'[^']*')+/g) ?? [];
return matches.map((token) => stripQuotes(token)).filter(Boolean);
}