CVE-2026-102268:PyJWT 非对称 PEM 检测绕过——变异公钥如何作为 HMAC 密钥通过验证
一句话结论:PyJWT 直到 2.13.0 用一个「严格结构正则」判断一段字节是不是非对称公钥,而底层 cryptography 库的 PEM 加载器接受的格式比它宽松得多。两者接受范围的差集,使公开公钥可以被当作 HMAC 对称密钥使用——只要验证端的算法允许列表同时混用 HMAC 与非对称算法、且配置里的密钥恰好是正则识别不出的「变异形态」,任何知道公钥的人都能伪造出通过验证的令牌。这是 CWE-347,属 JWT 算法混淆家族,是对前序防护的不完整实现被绕过。本文完整保留成因链、补丁原理与检测缓解,不复述可直接照抄的 PoC 字节与命令。
排版说明:正文按「现象 / 基本信息 / 成因链 / 触发前提 / 链路走查 / 补丁 / 缓解 / 检测 / 延伸」组织;引号统一用直角引号,换行符与回车符写作「LF(0x0A)」「CR(0x0D)」、制表符写作「制表符(0x09)」,正则与字节一律用文字描述、不写转义序列。
一、现象:同一秒、同一把密钥、三种结果
一台运行 PyJWT 2.13.0 的演示服务在同一秒内处理了五条验证请求,用的是同一把验证密钥。第一条是服务自己签发的合法 ES256 令牌,正常通过;第二条、第四条声明算法为 HS256,被拒(签名校验失败);第三条同样声明 HS256,签名它的人从来没有接触过任何私密密钥,却通过了;第五条把刚被接受的伪造令牌递给只允许 ES256 的端点,在密钥检查之前即被拒。
关键在第三条:它用来「签名」的「密钥」,就是从公开端点下载的公钥文本,只是照着配置管道的损坏方式把行尾重新替换了一遍。HMAC 是字节精确的——只要攻击者签名的字节和服务端验证的字节一位不差,重算结果自然逐位一致。决定通过还是拒绝的差异,就是本文要讲清楚的事。
二、漏洞基本信息
编号 CVE-2026-102268,GHSA 对应编号,类型归入 CWE-347(加密签名验证不当)。这是逻辑缺陷、不是内存破坏:攻击者拿到的是「本不该给他的越权令牌」,不是指令指针。受影响范围 PyJWT 不高于 2.13.0,修复版本 2.14.0。官方评分 CVSS 三点一为 9.1 CRITICAL,向量强调网络可达、复杂度低、无需权限、无需用户交互,危害在机密性与完整性都高(可伪造任意声明,即通用伪造)、不影响可用性。生态信号:SSVC 把可利用性标为「已有 PoC」、可自动化、技术影响「总计」;EPSS 约万分之二,无在野利用证据。披露是协同的,修复先于公开:约九月上旬合入修复提交、次日发布 2.14.0 并公开 GHSA,月底 CVE 记录与丰富化跟进。同批 2.14.0 还一并加固了 HMAC 密钥校验对 JWK、JWKS、数组、编码、带 BOM 前缀、DER、PEM 等多种输入的拒绝,暂不能升级时建议把这批公告一并纳入评估。
一个重要提醒:版本命中不等于可被利用。下面第四节那四项前置条件是「可被利用」的门槛,前三项是部署属性、攻击者运行时无法凭空制造,大量「版本命中但配置规范」的部署因第 1、2 项实际不可达,9.1 分的真实风险面要放在这个背景下看。
三、成因链:一张图——检测面与加载面的漂移
PyJWT 的验证栈自上而下四层:JWT 层只管声明的编解码与业务语义校验,把签名验证整体委托给 JWS 层;JWS 层只管紧凑序列化的拆装与按算法头分派;真正接触密钥字节、决定密钥如何被解释的,是最底层的算法对象。
分派的核心矛盾在一处:算法对象由令牌头里的算法字段选择(攻击者完全可控),验证密钥由部署配置提供。当允许列表同时容纳「接受任意字节」的 HMAC 族和「接受密钥对象」的非对称算法两族时,同一个密钥输入就可能被攻击者引导到错误的密码学语义上。PyJWT 的文档明确警告「不要把对称与非对称算法混在同一个 algorithms 里」,代码也实现了防护——防护设在 HMAC 算法的密钥预处理里:如果这串字节「形如非对称密钥」,就拒绝把它当 HMAC 密钥。这个判据是一个 PEM 结构正则加一个 SSH 前缀检查。
漏洞就出在这个「形如」判据上。它的 PEM 正则识别的是一整块 PEM 的整体结构,可以拆成四条硬约束:一,标记行必须以 LF 结尾(可选 CR 加必需 LF);二,END 标记的连字符必须紧贴在 LF 之后,中间不容任何字节;三,标签拼写精确、大小写敏感、分隔空白的位置有限;四,主体非空且结构上能切出「标记行—主体行—标记行」三段。一串字节要被正则认作 PEM,四条必须同时满足;而它要被攻击者利用,只需在违反其中任一条的同时,仍然被 cryptography 的加载器接受。
于是有三种「内容不变、结构形态改变」的公钥逃逸方向:在 END 标记前插入制表符,违反约束二(正则的字符类只认连字符和空格,制表符不在其中);把所有 LF 替换成裸 CR,违反约束一(整串里没有必需的那个 LF,无反溯余地);把整段折叠成单行,同样违反约束一(折叠后无换行可对)。三者都能骗过检测正则,却都仍被加载器接受——因为 RFC 7468 对 PEM 的规定本身是不对称的:它要求生成端严格,却明确允许解析端宽松(忽略多余空白、处理不同换行约定、接受最宽松的解析)。关卡另一半的 SSH 前缀检查更窄,只在偏移零处做前缀匹配,前面只要多出任何一个字节就失效。
非对称算法的密钥预处理根本不调用这个正则——它直接把字节交给加载器。所以同一串变异公钥,走非对称路径会被正常加载成合法密钥对象(正常业务照常工作、毫无报错),走 HMAC 路径却被正则判为「不是 PEM」、放行进入 HMAC 运算。这就是「双实现漂移」:同一个事实「什么是 PEM」存在两个副本,检测副本按严格生成面写、加载副本按宽松解析面实现,规范本身就包含这对矛盾,两者之差就是攻击面。
四、触发前提(四条同时满足)
一,算法允许列表混用 HMAC 与非对称算法(例如同时列出 ES256 与 HS256),这是最佳现行实践明令防范的反模式。二,验证密钥以原始 PEM 文本或字节传入(不走 JWK 对象路径),且是加载器接受、检测正则识别不出的「变异形态」——这类形态可自然产生于带缩进的配置块、单行字符串载体,或配置工具链把行尾误转换。三,已安装 cryptography(非对称验证与 PEM 解析都依赖它,未装时注册表里只有 none 和 HMAC 族)。四,攻击者知晓公钥——公钥本就公开,这一项几乎总是满足。
官方确认不受影响的路径有两条,正好对应消除前提一与二:JWK 密钥对象路径在构造时绑定单一算法、验证时强制算法与密钥算法一致,变异 PEM 进不了这条链;单一算法允许列表(不含 HMAC 族)会让伪造的令牌在允许列表检查处即被算法错误拒绝。另外要认清:密钥长度检查不是缓解——它的门槛只看 HMAC 摘要长度(HS256 为三十二字节),而公钥 PEM 远超此值,只看长度不看内容,开不开都拦不住。
五、链路走查:为什么后续校验救不了
签名一旦伪造成功,声明校验形同虚设。验证的顺序是「先验签、后验声明」,声明校验验证的是「内容是否自洽且符合策略」,它的有效性以「签名已证明内容出自密钥持有者」为前提。攻击者伪造的令牌格式规范、时间合理(把签发时间取当前、过期时间外推一小段窗口刚好卡在边界内)、类型正确(主体是合法字符串),八个声明校验器零次拦截——这不是哪个校验器写错,是校验顺序的固有含义。
两个可迁移的细节值得记。其一,这是「密钥分类」的 bug、不是「验证逻辑」的 bug:分类器识别不出公钥,于是签发与验证两端一起失效,攻击者伪造令牌时不需要绕过签名端任何检查(签名由库自身完成),也正因如此修复只需动一处、两端同时恢复防御。其二,在验证前读取未验签的令牌头只能用于审计日志,绝不能拿它去决定允许列表参数——那等于把算法选择权交给攻击者。整条验证链没有可变共享状态、无竞态窗口,攻击是每请求独立无状态的,一次成功即在密钥轮换前永久成功。
六、补丁原理:从结构正则换成标记配对状态机
修复把「什么是 PEM」的检测器从「严格结构正则」换成「标记配对状态机」:不再依赖换行、三段结构这些精确形态约束,而是扫描封装边界标记本身。效果是——第三象限的三个已知变异族(END 前空白、裸 CR、单行折叠)全部落入识别范围,「结构变形」这一整类逃逸方向随之消失,任何不破坏标记拼写的变形都不再能被绕过;复杂度顺带从最坏平方级降到线性;调用点零改动,签发与验证两端同时生效。运行时表现为同一批变异字节的检测判定从「不是 PEM」翻转为「是 PEM」,加签在密钥预处理即被拒。
残留的一层要看清。「什么是 PEM」仍然是两个独立实现,这次修复靠人工对齐了当前已知的接受面,没有建立任何自动同步机制;标签集仍是封闭的一组,加载器将来若接受新标签或更松拼写,缝隙理论上会重新出现。定级一句话:对「已识别的检测面缺口」是完整修复,对「双实现漂移」这一结构性问题只是部分缓解。约束外的彻底解(强制 JWK 算法绑定)留给未来的破坏性版本。
七、不能立即升级时的缓解(按消除前置条件排序)
一,算法允许列表单一化(消除前提一):从每个解码调用的允许列表里去掉与主算法不同族的成员,非对称验证端点只留对应非对称算法。一行配置改动,效果不必推测——受影响版本的对照组直接证明过:伪造令牌在允许列表检查处即被算法错误拒绝,根本走不到密钥检查。二,改走 JWK 密钥对象路径(消除前提二的「原始字节」这半):密钥构造时绑定算法、验证时强制一致。三,密钥形态规范化(消除前提二的「变异」这半):审计密钥从文件、配置、环境变量进入应用的整条管道,确保以规范 PEM(标准换行、标记前后无多余空白)传入,重点排查缩进配置块、单行字符串载体、老式行尾转换。四,监控辅助(四条里唯一不消除前置条件、只压缩「从被试到被发现」时间的一条)。
要认清 WAF 在这里帮不上忙:伪造令牌的签名是一次合法的 HMAC 计算、载荷是寻常声明集,流量层没有可拦的特征串;唯一暴露在流量里的信号是算法头,而混用列表的部署里 HMAC 本就可能是合法业务,网关统一拦截会先误伤合法业务。缓解的重点在代码与配置,不在流量层。
八、检测:如何判断是否已被利用
范围由版本划:不高于 2.13.0 且满足前置条件的才需要往下查,已有商业扫描插件做版本检测。暴露面由代码定:全库搜解码调用点,一看允许列表是否混用 HMAC 与非对称族,二看密钥实参是否为原始 PEM 字节而非 JWK 对象,两条都命中再核对密钥字节的实际形态是否规范,三层筛下来剩下的就是暴露面。
是否被试过要问日志。利用尝试有清晰指纹:一个业务上只有非对称令牌的端点,验证记录里出现 HMAC 算法——成功即二百,失败表现为签名校验错误(注意不是「密钥错误」,前者说明关卡已放行、只是签名对不上,攻击者已进入「换形态再试」的枚举阶段,这一条本身就是高价值告警);短时间内同一种失败密集反复、最终转二百,与在线枚举行为吻合。事后排查可在测试环境重放探针:取部署的实际验证密钥字节,打印检测正则的判定、再交给加载器解析一次,把两个读数并排——「判定为不是 PEM 且加载器接受、允许列表又混用」即构成「曾经可被利用」的技术确认;同一探针在升级后判定全部翻转,可兼作修复验证。
九、延伸思考:可模板化的审计问题
这个漏洞的发现思路可一般化为「差分测试」:拿同一批密钥形态,分别输入检测面与加载面,找出两者判定不一致的样本。任何「上游自写识别器 + 下游库真实接受面」的组合,都是潜在的差分测试目标;识别器判「不是」而下游判「是」的每一个样本,都是上游防护的盲区。顺着这条思路在同一版本里还能圈出至少三个同模式坐标:PEM 正则对加载器、SSH 前缀对 SSH 加载器、JWK 探测的 JSON 解析对密钥文档的真实边界。
把「双实现漂移」当成一类漏洞模式看:根因不在某行代码写错,而在同一事实存在两个副本、且没有同步机制。这类副本对到处都是——协议解析器与协议校验器、WAF 规则与后端解析(HTTP 请求走私的同款根因)、权限检查与资源寻址。可操作的检查是:凡是发现自己在「重新实现下游已经实现的判断」,先确认两个实现以哪个为准、能否让判断只有单一出处。
回到 JWT 生态的长期取舍:算法头由令牌携带、算法对象由头选择、密钥由部署提供,这套 API 是易用性的产物;而最佳现行实践要求「不要信任令牌头里的算法选择」,本质上是要求部署者替库承担结构性约束。这条教训可以概括为一句话——关卡的可靠性不取决于检查多严,而取决于识别面与接受面是否对齐;对齐维度错误的严格检查,比维度正确的宽松检查更危险,因为它制造虚假的安全感、同时放行真正的威胁。安全边界要分层:配置层消除混用、密钥管理层走 JWK 绑定、库层做形态规范化——任何单一层都不应被假设为足够。