CVE-2026-62379/57141OpenAM 与 PraisonAI 两条未认证 RCE 的同一个错误
摘要
同一天被推送到同一个渠道的两条漏洞,一条在 Java 的身份管理底座里,一条在 TypeScript 的 AI Agent 框架里,语言不同、生态不同、CVSS 相同(9.8),但它们的成因是同一个:
危险操作(反射实例化 / 代码执行)发生在一个本该由安全检查守卫的位置,而那个安全检查被放在了危险操作的下游。
CVE-2026-62379:OpenAM 的
/authservice端点在解析 XML 时,就把攻击者指定的类加载并实例化了。安全令牌的校验写在解析完成之后——校验执行的时候,恶意类的静态初始化器早就跑完了。
CVE-2026-57141:PraisonAI 的
codeMode工具用new Function()+with(sandbox)当沙箱。with只把对象挂进作用域链,不阻断向外查找,所以一段Function('return this')()就能取回真正的全局对象;而唯一的防线是一组正则黑名单,用字符串拼接即可绕过。
两者的共同教训不是"记得打补丁",而是:黑名单和伪沙箱不是安全边界。修复这两条漏洞的方式,都不是"把黑名单补全一点",而是把信任边界挪到正确的层级——OpenAM 把类名解析从"执行期"提到"加载期"并加类型约束,PraisonAI 从 new Function 换成 node:vm 的独立上下文。
CVE-2026-62379:解析期的反射污染
漏洞点
OpenAM 的远程认证端点 /authservice 使用 PLL(Parameter List Language)XML 协议。客户端在 <CustomCallback> 元素里可以指定一个 className 属性,服务端据此加载并实例化对应的回调类。修复前的代码是:
String className = XMLUtils.getNodeAttributeValue(childNode, AuthXMLTags.ATTRIBUTE_CLASS_NAME);
// ...
Class xmlClass = Class.forName(className); // ← className 完全可控
callback = (DSAMECallbackInterface) xmlClass.newInstance(); // ← 直接实例化两个细节让它比"任意类加载"更严重:
Class.forName(className)的默认语义会执行目标类的静态初始化器。攻击者不需要找到构造函数有副作用的类,只要目标 classpath 上存在任何一个静态块有副作用的类即可。
newInstance()随后调用无参构造函数。JNDI 注入类、Spring / CXF 相关 gadget 都属于这一类"静态块或构造器有副作用"的典型目标。
这个缺陷早于 Open Identity Platform 分叉,也就是说它同样影响其前身 ForgeRock OpenAM 的历史版本,受影响范围是"≤ 16.1.1 的全部发行版"。
为什么 sunRemoteAuthSecurityEnabled 不是缓解
这是整条漏洞里最值得记住的一点,也是很多二手转述会漏掉的一点。
OpenAM 有一个远程认证安全开关 sunRemoteAuthSecurityEnabled,直觉上它应该能拦住这类攻击。但调用顺序是这样的:
AuthXMLRequest.parseXML(...) ← 在这里读到 className 并实例化类
↓
AuthXMLHandler.processAuthXMLRequest(...) ← 在这里才校验安全令牌令牌校验在解析之后。 当它发现请求缺少有效令牌并拒绝时,Class.forName 和 newInstance 都已经执行完毕——攻击者的代码已经在 JVM 里跑过了。校验发生在污染之后,因此它拦住的只是一个已经完成攻击的请求。
官方公告的措辞很直接:Enabling sunRemoteAuthSecurityEnabled does not mitigate this issue.
值得记录的是,这条"不要依赖它"的缓解说明本身是被修正过的——公告最初给出的临时缓解里包含了这个开关,后来由 @BarakSrour 指出无效并修正。这说明连维护者在第一轮评估时也会犯"看到配置项就以为它是防线"的错误。
两条独立利用面
面 1:任意类加载与实例化。 即上面所述,通过 className 属性触发静态初始化器与无参构造。
面 2:序列化 <Subject> 的不安全反序列化。 AuthXMLUtils.getDeSerializedSubject() 对回传的 Subject 值直接调用 ObjectInputStream.readObject(),没有任何类白名单。如果该值可以被伪造或通过中继响应回填,即可走标准 gadget 链(如 CommonsCollections 系列)达成 RCE。
面 1 是"确定性"的(只要 classpath 上有合适的类),面 2 是"结构性"的(无白名单本身就是缺陷)。修复提交同时处理了两者。
修复与缓解
修复提交 edcf968 做了三件事,各堵一条路径:
处置顺序建议:
立即:确认
/authservice是否对外可达。这是唯一可靠的判定条件,与 CVSS 分数、EPSS 分数都无关。可达则封堵:该端点仅供内部 PLL 使用,正常部署不应暴露在公网。网络层封堵是官方认定的唯一可靠缓解。
可选:在反向代理或 WAF 拦截携带
<CustomCallback className="...">的请求。注意——该元素只在自定义DSAMECallbackInterface回调时才会产生,多数部署从不发送它,所以先比对自己环境的历史流量再强制,否则可能打断正常业务。根治:升级到 16.1.2。
失陷排查:审计
/authservice的访问日志、异常类加载行为、可疑外联与Subject反序列化痕迹。鉴于这是一条"未认证、默认配置可打"的路径,建议直接按"可能已被控制"处理,轮换服务凭据与 SSO 信任材料。
自查命令(仅做可达性判断,不发送利用载荷):
# 1. 版本确认(< 16.1.2 均受影响)
curl -s https://sso.example.com/amserver/version
# 2. 可达性确认:观察是否在鉴权之前就进入 PLL 解析
curl -s -o /tmp/am.out -w '%{http_code}\n' \
-X POST https://sso.example.com/authservice \
-H 'Content-Type: text/xml' \
--data-binary '<AuthContext version="1.0"/>'
head -c 400 /tmp/am.out
# 判读:
# 200 / 500 且响应体含 PLL 解析痕迹 → 未认证可达,按最高优先级封堵
# 401 / 403 → 当前被拦住,但仍按升级计划处理CVE-2026-57141:伪沙箱与黑名单幻觉
一个自称沙箱的 with
PraisonAI 的 codeMode 工具让 Agent"写代码 → 跑代码",并且在自己的能力声明里标注 capabilities.sandbox: true。它的实现是(code-mode.ts L187–191):
const sandbox = {
console: { log: ..., error: ..., warn: ... },
process: undefined, // ← 只是把这个对象里的属性置空
require: undefined, // ← 无法阻止沿全局作用域取回
env: env || {}, files: files || {},
};
const fn = new Function('sandbox', `with (sandbox) { ${code} }`);
const result = fn(sandbox); // ← 在宿主 V8 上下文里执行执行前只过一组正则黑名单(L108–136):
const blockedPatterns = [
/require\s*\(\s*['"]child_process['"]\s*\)/,
/require\s*\(\s*['"]fs['"]\s*\)/,
/import\s+.*from\s+['"]child_process['"]/,
/process\.exit/,
/eval\s*\(/,
];这里有三处根本缺陷,任何一处都足以致命:
第一,with 不提供隔离。 JavaScript 的 with 语句把对象加入作用域链,但从不阻断对全局对象的访问。process: undefined 和 require: undefined 只是遮蔽(shadowing),不是禁止(denial)——作用域链查找失败后会继续向外穿透,直到全局。Function('return this')() 或 ({}).constructor.constructor(...) 就能拿回真正的 Function 构造器,进而拿到真实全局对象。
第二,黑名单可被字符串拼接绕过。 正则要求 require( 与 ) 之间出现字面量 'child_process'。改成 g.require('child_' + 'process') 后,正则看到的是变量拼接,不是字面量,完全绕过。
第三,关键逃逸原语根本没进黑名单。 列表里不含 Function(、new Function、constructor、__proto__、prototype、return this、global、globalThis、global.require。
取回 process 之后,CommonJS 环境下 process.mainModule.require 即可加载 fs / child_process,越过一切显式控制。
官方公告里的实测输出
GitHub 安全公告(GHSA-p69m-4f92-2v84,测试环境 Node.js v20.20.0)给出的已观测结果是:
OUT: process.version: v20.20.0
OUT: RCE: uid=1000(sondt23) gid=1000(sondt23) groups=1000(sondt23),4(adm),...,983(docker),984(ollama)这个对照很有价值:它证明问题不是"黑名单没生效",而是"黑名单生效了,但它拦的不是逃逸路径"。这正是所有基于黑名单的防护的典型失败模式。
触发面:从提示词到 shell
这条漏洞的攻击面描述起来很短:只要攻击者能影响 codeMode 的 code 参数。而 AI Agent 场景下,影响这个参数的方式多得离谱:
提示词注入(用户输入或上游系统拼接的文本)
不可信文档(Agent 读进来的 PDF / 网页 / 邮件)
被投毒的 MCP 工具返回(Agent 信任自己调用的工具的输出)
也就是说,一个"读文档 → 总结 → 顺手写点代码"的常规 Agent 流程,就可能变成一条从外部文本到宿主 shell 的直通车。公告作者把它总结为"提示词 → 代码执行"的直连路径,并不夸张。
修复与缓解
修复版本 1.7.2 的做法是换掉机制,而不是补黑名单:
codeMode改用node:vm的runInNewContext,在新建的隔离上下文中执行,并加超时;
shell工具改用spawn(shell: false),并拦截 shell 元字符。
处置顺序建议:
立即:确认
codeMode是否启用。非必要就关掉——这是最有效的一步。升级:
praisonai-ts≥ 1.7.2。真正的隔离:如果确实需要执行 LLM 生成的代码,用
vm上下文 /isolated-vm(独立 V8 isolate)/ 独立子进程,不要依赖new Function+ 黑名单。最小权限:运行 Agent 的进程降权、移出
docker组、去掉宿主机挂载、剥离云 IAM 权限、限制容器出网。公告里那条groups=...,983(docker),984(ollama)提示了容器内横向移动的现实风险。输入侧:对提示词、外部文档、MCP 返回做来源可信标记,把不可信内容隔离在无法触发工具调用的通道里。
HITL:对高权限工具调用开启人工审批。
自查命令:
# 依赖版本
npm ls praisonai
# codeMode 是否被启用
grep -rn "codeMode\|code_mode" . --include=*.ts --include=*.js --include=*.json | head -20
# 确认不在使用旧实现
grep -rn "with (sandbox)" node_modules/praisonai/dist 2>/dev/null关于黑名单的补充说明: 即使要保留黑名单,也必须补上 Function(、new Function、constructor、__proto__、prototype、return this、global、globalThis。但请把它当降低噪声的手段,而不是安全边界——正则黑名单对"字符串拼接"和"原型链"这两类绕过天生无力。
处置优先级
两条漏洞的 CVSS 都是 9.8,但处置顺序不该由 CVSS 决定。判据是网络暴露面和触发前置条件:
一个容易被忽略的量:CVE-2026-62379 的 EPSS 约为 0.65%(50 分位),即模型预测其未来 30 天被利用的概率偏低。但 EPSS 是"全网被利用概率",不是"你的实例被利用概率"——如果你的 /authservice 在公网,这个数字对你的参考价值接近于零。
与二手推送稿的差异
本文核对了 GHSA 官方公告,发现流传的推送稿存在以下偏差,一并列出以便判断:
第三条偏差最值得注意。把两个月前的公告包装成"当日新披露",会让人误判处置窗口——你以为还有几天,其实公告已经挂了两个月。
可迁移的结论
抛开这两个具体组件,这次分析里有三条值得记进方法论的东西:
1. 检查点必须早于污染点,否则等于没有检查。 sunRemoteAuthSecurityEnabled 这个开关的存在,本身会给人"我已经做了防护"的错觉,而它实际拦住的只是一个攻击已经完成的请求。评估任何安全控制时,第一个要问的问题不是"它检查什么",而是"它什么时候检查"。
2. 黑名单是噪声过滤器,不是安全边界。 PraisonAI 的黑名单确实拦住了直连写法——它工作得很正常。问题在于逃逸路径根本不经过它检查的那些字符串形态。任何依赖"枚举坏东西"的防护,都会在攻击者换一种写法时失效;只有"枚举好情况"的白名单(如 ObjectInputFilter 的类白名单)才有机会站住。
3. 声明式的能力标签不可信。 capabilities.sandbox: true 是一个字符串常量,不是隔离保证。判断一个沙箱是否真实,唯一的方法是看它的实现机制——with 不是隔离,vm 上下文是(相对而言),独立 V8 isolate / 子进程更是。看机制,不看标签。