# Vulnerability Verification Report

## 1. Summary
- **Vulnerability Type**: OS Command Injection (CWE-78)
- **Flagged Location**: `src/infra/exec-approvals-analysis.ts`
- **Vulnerability Description**: OpenClaw versions prior to 2026.2.22 contain an allowlist bypass vulnerability in `system.run` that allows attackers to execute non-allowlisted commands by splitting command substitution using shell line-continuation characters (`\` followed by newline) inside double quotes. The shell parser (`splitShellPipeline`) treats `\` + newline as a valid escape sequence inside double quotes and fails to detect the `$(` command substitution pattern that forms after the line continuation is folded by the actual shell.

## 2. Analysis Logic

### Step 1: Examining the flagged sink in `src/infra/exec-approvals-analysis.ts:391-414`
I read the `splitShellPipeline` function which is responsible for parsing shell commands before allowlist evaluation. Inside the double-quote handling block:

```typescript
if (inDouble) {
  if (ch === "\\" && isDoubleQuoteEscape(next)) {
    buf += ch;
    buf += next;
    i += 1;
    emptySegment = false;
    continue;
  }
  if (ch === "$" && next === "(") {
    return { ok: false, reason: "unsupported shell token: $(", segments: [] };
  }
  if (ch === "`") {
    return { ok: false, reason: "unsupported shell token: `", segments: [] };
  }
  if (ch === "\n" || ch === "\r") {
    return { ok: false, reason: "unsupported shell token: newline", segments: [] };
  }
  if (ch === '"') {
    inDouble = false;
  }
  buf += ch;
  emptySegment = false;
  continue;
}
```

**Analysis**: The parser attempts to block command substitution `$(` and newlines inside double quotes. However, the `isDoubleQuoteEscape` check (line 392) consumes a backslash followed by newline as a valid escape sequence, preventing the newline rejection at line 405. Because `$` and `(` are now consumed in separate loop iterations (with `\n` in between), the `$(` check at line 399 never fires.

**Next**: I need to verify the `DOUBLE_QUOTE_ESCAPES` set and trace how this leads to a real allowlist bypass.

### Step 2: Checking the escape set definition in `src/infra/exec-approvals-analysis.ts:250-267`
```typescript
const DOUBLE_QUOTE_ESCAPES = new Set(["\\", '"', "$', "`", "\n", "\r"]);

function isDoubleQuoteEscape(next: string | undefined): next is string {
  return Boolean(next && DOUBLE_QUOTE_ESCAPES.has(next));
}
```

**Analysis**: The `DOUBLE_QUUE_ESCAPES` set explicitly includes newline (`\n`) and carriage return (`\r`). When inside double quotes, if the parser sees `\` followed by `\n`, it treats it as a valid escape sequence and consumes both characters without rejecting the command.

**Next**: I need to confirm that the actual shell (`/bin/sh`) removes `\` + newline inside double quotes, causing a semantic mismatch between parser and shell.

### Step 3: Confirming shell behavior mismatch
In POSIX-compliant shells (including `/bin/sh`), inside double quotes, a backslash followed by a newline is treated as a line continuation and is removed. For example:
- Input to parser: `echo "$(\\
id)"`
- Parser sees: `echo "$(\<newline>id)"` → consumes `\` + newline as escape → `$` and `(` are in separate iterations → `$(` check never triggers → parser returns `ok: true`
- Actual shell sees: `echo "$(id)"` (line continuation removed) → executes `id` via command substitution

**Analysis**: This is a genuine parser/shell semantic mismatch. The parser thinks the command is safe, but the shell executes arbitrary code via command substitution.

**Next**: Trace the data flow from user input to the vulnerable parser to confirm reachability.

### Step 4: Tracing data flow from user input to `splitShellPipeline`
1. **Entry point**: `src/agents/bash-tools.exec.ts:858` - the `exec` tool receives `params.command` from the agent (user-controllable).
2. **Gateway path**: At line 1058, `evaluateShellAllowlist` is called for `host=node`:
```typescript
const baseAllowlistEval = evaluateShellAllowlist({
  command: params.command,  // user input
  allowlist: [],
  safeBins: new Set(),
  cwd: workdir,
  env,
  platform: nodeInfo?.platform,
});
```
3. **Parser**: `evaluateShellAllowlist` at `src/infra/exec-approvals-analysis.ts:1036` calls `splitCommandChain` (line 1046), which delegates to `splitShellPipeline` (line 707).
4. **Node execution**: If the parser returns `ok: true`, the command passes to `buildNodeShellCommand` at `src/infra/node-shell.ts:8`:
```typescript
return ["/bin/sh", "-lc", command];
```
The command is executed via `spawn(argv[0], argv.slice(1), ...)` in `src/node-host/invoke.ts:241`.

**Analysis**: User-controllable input reaches the vulnerable parser, and a bypassed allowlist leads to arbitrary command execution on the node.

**Next**: Check if the project version is before the fix.

### Step 5: Checking project version in `package.json:3`
```json
{
  "name": "openclaw",
  "version": "2026.2.13",
  ...
}
```

**Analysis**: The current version is `2026.2.13`, which is **before** the patched version `2026.2.22` mentioned in the CVE. This confirms the vulnerability is present.

**Next**: Check for any existing tests that would prevent this.

### Step 6: Checking test coverage for line-continuation inside double quotes
I read `src/infra/exec-approvals.test.ts` in full. Tests exist for:
- `$(whoami)` inside double quotes → correctly rejected
- Backticks inside double quotes → correctly rejected
- Escaped `\$(whoami)` → allowed
- Command substitution in single quotes → allowed

However, there is **no test** for the line-continuation bypass pattern: `$\` followed by newline and `(` inside double quotes.

**Analysis**: The missing test coverage confirms this bypass path is unprotected.

### Step 7: Constructing a concrete bypass payload
A concrete exploit payload for the gateway→node path would be:
```
echo "$(\ 
id)"
```
(where `\ ` is actually `\` followed by a newline)

When parsed by `splitShellPipeline`:
1. `echo ` → added to buf
2. `"` → `inDouble = true`
3. `$` → buf += `$`
4. `\` followed by `\n` → consumed as escape sequence, buf += `\`, buf += `\n`
5. `(` → buf += `(`
6. `id` → buf += `id`
7. `)` → buf += `)`
8. `"` → `inDouble = false`

The parser returns `ok: true` because the `$` and `(` were consumed in separate iterations, and the `$(` check never triggers.

When executed by `/bin/sh -lc`:
The shell removes `\` + newline inside double quotes, producing `echo "$(id)"`, which executes `id` via command substitution.

### Analysis Process
- Q1: Does user-controllable input reach a command execution sink? → **YES**. `params.command` from the `exec` tool (user-controllable) flows through `evaluateShellAllowlist` and eventually to `spawn()` in `src/node-host/invoke.ts:241`. Evidence: Steps 1-4 above.
- Q2: Is it code evaluation (eval/exec) with user data? → **NO**. It is process execution via `/bin/sh -lc`.
- Q3: Is the command executed in array form without shell? → **NO**. The command is explicitly wrapped with `sh -lc` at `src/infra/node-shell.ts:8`, invoking a shell.
- Q4: Is the user input effectively sanitized? → **NO**. The `splitShellPipeline` parser has a bypass: it treats `\` + newline inside double quotes as an escape sequence, which breaks the `$(` detection. This creates a parser/shell semantic mismatch.
- Q5: Is the code in test/demo/dead/generated context? → **NO**. This is production code handling `system.run` on the node host path.
- → Reached leaf: **TRUE_POSITIVE**

## 3. Verdict
**TRUE_POSITIVE**

**Key Evidence:**
- The `splitShellPipeline` parser in `src/infra/exec-approvals-analysis.ts:391-414` consumes `\` followed by newline as a valid double-quote escape sequence (via `DOUBLE_QUOTE_ESCAPES` which includes `\n` and `\r` at line 251).
- Inside double quotes, this causes `$` and `(` to be consumed in separate loop iterations, so the `$(` detection at line 399 never fires.
- The actual `/bin/sh -lc` shell removes `\` + newline inside double quotes (POSIX line continuation), causing the shell to see `$(command)` and execute command substitution.
- User input from the `exec` tool (`src/agents/bash-tools.exec.ts:858`) flows directly to this parser, and the current version (`2026.2.13` in `package.json:3`) is before the patched version `2026.2.22`.
- No test exists for this line-continuation bypass pattern in `src/infra/exec-approvals.test.ts`.

## 4. Remediation Suggestions
- **Fix the parser**: In `splitShellPipeline`, when inside double quotes, do not allow `\` + newline to break the `$(` detection. One approach is to track whether a `$` was immediately followed by a line continuation, and if so, check whether the next non-whitespace character is `(`. Alternatively, reject `\` followed by newline inside double quotes entirely, since it is a shell metacharacter that can change command semantics.
- **Normalize before parsing**: Pre-process the command string to fold shell line continuations (`\` + newline) before running the allowlist parser, so the parser sees the same command the shell will see.
- **Add test coverage**: Add a test case in `src/infra/exec-approvals.test.ts` for the exact bypass pattern: `echo "$(\\
id)"` should be rejected.
- **Upgrade**: Upgrade to OpenClaw version `2026.2.22` or later which contains the fix for this CVE.
