The “Read-Only” Tool That Runs Shell: code-ollama’s grep_search Auto-Executes Injected Commands (GHSA-456v-xq2p-r4cj)

GHSA-456v-xq2p-r4cj was published by the ai-action/code-ollama maintainer on 24 June 2026 and reviewed into the GitHub Advisory Database on 28 September 2026. It carries no CVE ID, is rated High at CVSS 7.8 (CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H) under CWE-78, affects code-ollama <= 0.36.0 on npm, and is fixed in 0.36.1. The package — a terminal coding agent driven by an Ollama backend — is currently at 0.53.8, so the patched line is many releases old. The interesting part is not the injection. It is who supplies the payload and why nobody was asked.

The bug: two characters escaped, one class forgotten

Per the advisory, the grep_search tool built a shell command string by interpolating the model-supplied pattern and path arguments and handing the result to child_process.exec(). The sanitisation replaced backslashes and double quotes — and nothing else. Command substitution via $(…) and backtick expansion passed through untouched, and because exec routes the string through /bin/sh, the shell obligingly evaluated them.

This is the oldest injection shape in the book, and the fix is equally old: do not build a shell string at all. Pass rg its pattern and path as an argument vector (execFile/spawn with an array, no shell), and the question of which metacharacters to escape stops existing. Escaping is a denylist; argument vectors are a structural guarantee. Every allowlist of “dangerous characters” eventually ships with one missing.

The threat model inversion: the model is the attacker

The advisory is explicit that the injection source is “a malicious or compromised Ollama server.” That sentence deserves to be read slowly, because it inverts the assumption most local-agent deployments are built on.

A terminal coding agent treats its model endpoint as trusted infrastructure — it is, after all, the thing whose reasoning the user asked for. But the tool-call arguments in a streamed response are just attacker-controllable bytes the moment the endpoint is not yours: a self-hosted Ollama instance on a shared network, a colleague’s box, a container pulled from a registry, a reverse proxy someone added for “caching,” or a model whose weights were tuned to emit a particular tool call under a particular trigger. The trust boundary was drawn around the network connection, not around the data crossing it. That is the same shape as the OpenCode upgrade-endpoint RCE and the Copilot CLI shell-expansion RCE (CVE-2026-29783): the agent’s own plumbing turns model output into process execution.

The real amplifier: “read-only” is a security classification nobody audits

The detail that turns a 7.8 into an operational problem is the approval path. The advisory notes grep_search sits in the agent’s read-tool set and is exposed in Plan mode — and read-only tools execute automatically, without an approval prompt. So the exploitation path requires no user interaction beyond starting the agent. The human-in-the-loop control that the entire agent-safety consensus rests on was never invoked, because a list somewhere said this tool could not do harm.

That list is a security boundary, and almost nobody treats it like one. “Read-only” in these codebases means the tool’s intent is to read. It does not mean the implementation lacks a write primitive. A search tool that shells out has the full authority of the shell; a file-lister that globs through a shell does too; a “fetch this URL” helper with a redirect follower reaches the metadata service. Intent is not a capability bound. The only defensible definition of read-only is this code path cannot invoke an interpreter, cannot write the filesystem, and cannot open a socket — and that is a property you assert by construction, not by naming.

We keep finding the same failure in different costumes: an approval token that does not bind what it approves, and now an approval step that is skipped entirely because of a label. Both are checks that do not check what they claim to.

What to do today

  • Upgrade code-ollama past 0.36.0 — 0.36.1 carries the fix, and the current release line is 0.53.8. Check global npm installs, not just project lockfiles; terminal agents are usually installed with --global and never audited again.
  • Audit your own agent’s read-tool allowlist as a security control. For every tool marked auto-executable, trace the sink. If any path reaches exec, system, eval, a template-rendered SQL string, or an outbound socket, it is not read-only — move it behind approval or rewrite the sink.
  • Ban shell-string construction in tool implementations. Argument vectors with shell: false, always. Add a lint or CI grep for child_process.exec( with a template literal; it is a cheap, high-yield rule.
  • Treat the model endpoint as untrusted input. Validate tool-call arguments against a schema with character classes at the dispatcher, before any tool sees them — the dispatcher is the one place that covers every tool at once.
  • Track no-CVE advisories. This one has no CVE ID, so CVE-keyed scanners will not surface it; the shrinking coverage of CVE-indexed feeds makes GHSA and OSV ingestion a requirement rather than a nicety.

Sources: