摘要

同一天被推送到同一个渠道的两条漏洞,一条在 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 请求处理顺序:危险操作发生在鉴权之前 FIG 1危险操作发生在鉴权之前OpenAM /authservice · AuthXMLRequest.parseXML → AuthXMLUtils.createCustomCallback Class xmlClass = Class.forName(className);// className 来自 XML 属性,无白名单 POST /authservice 未鉴权 createCustomCallback() 实例化任意类 processAuthXMLRequest() 校验安全令牌 已可执行代码 攻击者目标达成 1 2 3 4 校验点晚于污染点,开启 sunRemoteAuthSecurityEnabled 不构成缓解 修复 edcf968:Class.forName(name, false) + 强制 DSAMECallbackInterface + 反序列化白名单

漏洞点

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();     // ← 直接实例化

两个细节让它比"任意类加载"更严重:

  1. Class.forName(className)默认语义会执行目标类的静态初始化器。攻击者不需要找到构造函数有副作用的类,只要目标 classpath 上存在任何一个静态块有副作用的类即可。

  1. newInstance() 随后调用无参构造函数。JNDI 注入类、Spring / CXF 相关 gadget 都属于这一类"静态块或构造器有副作用"的典型目标。

这个缺陷早于 Open Identity Platform 分叉,也就是说它同样影响其前身 ForgeRock OpenAM 的历史版本,受影响范围是"≤ 16.1.1 的全部发行版"。

为什么 sunRemoteAuthSecurityEnabled 不是缓解

这是整条漏洞里最值得记住的一点,也是很多二手转述会漏掉的一点。

OpenAM 有一个远程认证安全开关 sunRemoteAuthSecurityEnabled,直觉上它应该能拦住这类攻击。但调用顺序是这样的:

AuthXMLRequest.parseXML(...)              ← 在这里读到 className 并实例化类
    ↓
AuthXMLHandler.processAuthXMLRequest(...) ← 在这里才校验安全令牌

令牌校验在解析之后。 当它发现请求缺少有效令牌并拒绝时,Class.forNamenewInstance 都已经执行完毕——攻击者的代码已经在 JVM 里跑过了。校验发生在污染之后,因此它拦住的只是一个已经完成攻击的请求。

官方公告的措辞很直接:Enabling sunRemoteAuthSecurityEnabled does not mitigate this issue.

值得记录的是,这条"不要依赖它"的缓解说明本身是被修正过的——公告最初给出的临时缓解里包含了这个开关,后来由 @BarakSrour 指出无效并修正。这说明连维护者在第一轮评估时也会犯"看到配置项就以为它是防线"的错误。

两条独立利用面

面 1:任意类加载与实例化。 即上面所述,通过 className 属性触发静态初始化器与无参构造。

面 2:序列化 <Subject> 的不安全反序列化。 AuthXMLUtils.getDeSerializedSubject() 对回传的 Subject 值直接调用 ObjectInputStream.readObject()没有任何类白名单。如果该值可以被伪造或通过中继响应回填,即可走标准 gadget 链(如 CommonsCollections 系列)达成 RCE。

面 1 是"确定性"的(只要 classpath 上有合适的类),面 2 是"结构性"的(无白名单本身就是缺陷)。修复提交同时处理了两者。

修复与缓解

OpenAM 修复提交 edcf968 的改动示意:把校验提前到加载时 FIG 3修复把校验提前到加载时commit edcf968 · AuthXMLUtils.java(示意,非逐字 diff) - Class xmlClass = Class.forName(className); // 攻击者可控,且触发静态初始化 - callback = (DSAMECallbackInterface) xmlClass.newInstance(); // 未校验类型 + Class c = Class.forName(className, false, loader); // false = 不触发静态初始化 + if (!DSAMECallbackInterface.class.isAssignableFrom(c)) throw … // 强制接口 + callback = (DSAMECallbackInterface) c.newInstance(); // 类型已收窄 + ObjectInputFilter f = subjectFilter(); // 反序列化白名单 三条修复各堵一条路径:不触发初始化 · 强制接口 · 反序列化白名单

修复提交 edcf968 做了三件事,各堵一条路径:

改动

堵住的路径

Class.forName(className, false, loader)

false = 不执行静态初始化器

强制 DSAMECallbackInterface.class.isAssignableFrom(c) 才实例化

任意类实例化

Subject 反序列化加 ObjectInputFilter 白名单(仅 Subject / JDK 集合标量 / Principal 实现)

gadget 链反序列化

处置顺序建议:

  1. 立即:确认 /authservice 是否对外可达。这是唯一可靠的判定条件,与 CVSS 分数、EPSS 分数都无关。

  2. 可达则封堵:该端点仅供内部 PLL 使用,正常部署不应暴露在公网。网络层封堵是官方认定的唯一可靠缓解

  3. 可选:在反向代理或 WAF 拦截携带 <CustomCallback className="..."> 的请求。注意——该元素只在自定义 DSAMECallbackInterface 回调时才会产生,多数部署从不发送它,所以先比对自己环境的历史流量再强制,否则可能打断正常业务。

  4. 根治:升级到 16.1.2。

  5. 失陷排查:审计 /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:伪沙箱与黑名单幻觉

PraisonAI codeMode 的 with(sandbox) 伪沙箱与作用域链逃逸 FIG 2with(sandbox) 不是隔离边界praisonai-ts 1.7.1 · src/tools/builtins/code-mode.ts L187-191 new Function('sandbox', `with(sandbox){ … }`) Function('return this')() → globalThis 'child_' + 'process' → 绕过正则 g.require(mod).execSync('id') → RCE // 黑名单只拦字面量 require('child_process') 作用域链(由内向外查找) with (sandbox) process: undefined 模块作用域 CommonJS 包装函数 globalThis 真实 process · require 逃逸路径 作用域链只做名字查找,不阻断向外穿透修复 1.7.2:改用 node:vm runInNewContext

一个自称沙箱的 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: undefinedrequire: undefined 只是遮蔽(shadowing),不是禁止(denial)——作用域链查找失败后会继续向外穿透,直到全局。Function('return this')()({}).constructor.constructor(...) 就能拿回真正的 Function 构造器,进而拿到真实全局对象。

第二,黑名单可被字符串拼接绕过。 正则要求 require() 之间出现字面量 'child_process'。改成 g.require('child_' + 'process') 后,正则看到的是变量拼接,不是字面量,完全绕过

第三,关键逃逸原语根本没进黑名单。 列表里不含 Function(new Functionconstructor__proto__prototypereturn thisglobalglobalThisglobal.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

这条漏洞的攻击面描述起来很短:只要攻击者能影响 codeModecode 参数。而 AI Agent 场景下,影响这个参数的方式多得离谱:

  • 提示词注入(用户输入或上游系统拼接的文本)

  • 不可信文档(Agent 读进来的 PDF / 网页 / 邮件)

  • 被投毒的 MCP 工具返回(Agent 信任自己调用的工具的输出)

也就是说,一个"读文档 → 总结 → 顺手写点代码"的常规 Agent 流程,就可能变成一条从外部文本到宿主 shell 的直通车。公告作者把它总结为"提示词 → 代码执行"的直连路径,并不夸张。

修复与缓解

修复版本 1.7.2 的做法是换掉机制,而不是补黑名单

  • codeMode 改用 node:vmrunInNewContext,在新建的隔离上下文中执行,并加超时;

  • shell 工具改用 spawn(shell: false),并拦截 shell 元字符。

处置顺序建议:

  1. 立即:确认 codeMode 是否启用。非必要就关掉——这是最有效的一步。

  2. 升级praisonai-ts ≥ 1.7.2。

  3. 真正的隔离:如果确实需要执行 LLM 生成的代码,用 vm 上下文 / isolated-vm(独立 V8 isolate)/ 独立子进程,不要依赖 new Function + 黑名单

  4. 最小权限:运行 Agent 的进程降权、移出 docker 组、去掉宿主机挂载、剥离云 IAM 权限、限制容器出网。公告里那条 groups=...,983(docker),984(ollama) 提示了容器内横向移动的现实风险。

  5. 输入侧:对提示词、外部文档、MCP 返回做来源可信标记,把不可信内容隔离在无法触发工具调用的通道里。

  6. 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 Functionconstructor__proto__prototypereturn thisglobalglobalThis。但请把它当降低噪声的手段,而不是安全边界——正则黑名单对"字符串拼接"和"原型链"这两类绕过天生无力。

处置优先级

按网络暴露面与触发前置条件排序的处置优先级 FIG 4 按暴露面排序,而不是按 CVSS 图中仅两条经过独立核实,同批补丁项未核实 立即处置 无需认证 需要交互 内网 / 不可达 公网可达 OpenAM /authservice 无需认证 · 默认部署即可达 PraisonAI codeMode 需攻击者影响 Agent 输入 纵轴 = 触发前置条件,横轴 = 网络暴露面。EPSS 仅作参考,可达性才是判据。

两条漏洞的 CVSS 都是 9.8,但处置顺序不该由 CVSS 决定。判据是网络暴露面和触发前置条件:

优先级

对象

判据

动作

最高

OpenAM ≤ 16.1.1

/authservice 是否公网可达

可达即封堵;升级 16.1.2;按可能失陷排查

PraisonAI codeMode

是否启用 + 是否处理不可信输入

关闭或升级 1.7.2;进程降权;输入隔离

参考

同批补丁项(Casdoor / pig / proxy-addr / Kimai 等)

本文未核实

按各项目公告独立处理

一个容易被忽略的量:CVE-2026-62379 的 EPSS 约为 0.65%(50 分位),即模型预测其未来 30 天被利用的概率偏低。但 EPSS 是"全网被利用概率",不是"你的实例被利用概率"——如果你的 /authservice 在公网,这个数字对你的参考价值接近于零。

与二手推送稿的差异

本文核对了 GHSA 官方公告,发现流传的推送稿存在以下偏差,一并列出以便判断:

项目

推送稿表述

核实结果

CVE-2026-62379 存在性与细节

OpenAM 未认证 RCE

属实。GHSA-wg5r-wc3x-39vc,CVSS 9.8,CWE-94 / CWE-470

sunRemoteAuthSecurityEnabled 无效

明确提示不可依赖

属实,且该条缓解说明经 @BarakSrour 修正

修复提交 edcf968 的内容

三项加固

与公告 Remediation 一致

发现者

Zhixi "Jace" Sun(TikTok ASM/VI)

属实

CVE-2026-57141 存在性与细节

PraisonAI codeMode 逃逸

属实。GHSA-p69m-4f92-2v84,CVSS 9.8,CWE-94

已观测 PoC 输出

真实用户 + docker 组

逐字一致

"公开时间 2026-09-15"

两条均标 9-15

不准确。OpenAM 公告发布于 2026-07-23,PraisonAI 发布于 2026-06-17。推送稿是旧公告的重新汇编,不是新披露

"优先级最高"

两条并列最高

需修正。OpenAM 的 EPSS 仅 0.65%,实际紧急度取决于自身暴露面

PoC 可用性

提供 Python / Bash 脚本

作者自述"报文封装需按目标 PLL 实现补全",属示意性 PoC;示例 gadget 类依赖目标 classpath 存在 Spring

CVE-2026-57138

CVSS 9.9,PR:L / Scope:Changed

本文未独立核实

第三条偏差最值得注意。把两个月前的公告包装成"当日新披露",会让人误判处置窗口——你以为还有几天,其实公告已经挂了两个月。

可迁移的结论

抛开这两个具体组件,这次分析里有三条值得记进方法论的东西:

1. 检查点必须早于污染点,否则等于没有检查。 sunRemoteAuthSecurityEnabled 这个开关的存在,本身会给人"我已经做了防护"的错觉,而它实际拦住的只是一个攻击已经完成的请求。评估任何安全控制时,第一个要问的问题不是"它检查什么",而是"它什么时候检查"。

2. 黑名单是噪声过滤器,不是安全边界。 PraisonAI 的黑名单确实拦住了直连写法——它工作得很正常。问题在于逃逸路径根本不经过它检查的那些字符串形态。任何依赖"枚举坏东西"的防护,都会在攻击者换一种写法时失效;只有"枚举好情况"的白名单(如 ObjectInputFilter 的类白名单)才有机会站住。

3. 声明式的能力标签不可信。 capabilities.sandbox: true 是一个字符串常量,不是隔离保证。判断一个沙箱是否真实,唯一的方法是看它的实现机制——with 不是隔离,vm 上下文是(相对而言),独立 V8 isolate / 子进程更是。看机制,不看标签。