0xmrma/CVE-2026-33936
GitHub: 0xmrma/CVE-2026-33936
完整披露并复现了 python-ecdsa 库中因 DER 长度字段验证不当导致的拒绝服务漏洞(CVE-2026-33936),包含根因分析、双 PoC、最小化补丁与回归测试。
Stars: 1 | Forks: 0
# CVE-2026-33936
ecdsa (PyPI) 中的拒绝服务漏洞
## 简介
我在 **python-ecdsa** 中发现并负责任地披露了一个**中等严重性**漏洞,这是一个被广泛使用的 Python 密码学库,**上个月下载量高达 4780 万次**。
我在审查 **python-ecdsa** 时发现了这个问题,当时我脑海中有一个非常具体的问题:
**如果格式错误的 DER 在长度上撒谎,而解析器又过于信任它,会发生什么?**
在这个案例中,这个问题引发了一个真实的 Bug。
当编码长度声明的字节数多于实际存在的字节数时,`ecdsa.der` 中的 DER 解析辅助函数接受了截断的数据。这种格式错误的输入本应立即被拒绝。然而,它能通过更深层的解析逻辑,并最终在密钥解析期间触发内部的 `IndexError`。
这个问题成为了 **CVE-2026-33936**。
**项目:** [GitHub 上的 python-ecdsa](https://github.com/tlsfuzzer/python-ecdsa)
**包:** `ecdsa` (pip)
**CVE:** CVE-2026-33936
## 攻击链
`攻击者控制的格式错误 DER → 截断的长度被接受为有效 → 解析器越过信任边界 → SigningKey.from_der() 到达内部异常路径 → 意外的 IndexError / 应用层 DoS 风险`
## python-ecdsa 的作用
**python-ecdsa** 是一个广泛使用的 Python 椭圆曲线密码库。
除其他功能外,它还负责处理:
- 密钥解析
- 密钥序列化
- DER/ASN.1 解码
- 签名和验证工作流
这意味着其解析代码直接位于安全边界上。
每当库接受外部提供的密钥材料或结构化二进制输入时,正确性就不再仅仅是一个质量问题。
它是一项安全属性。
如果在应该拒绝格式错误输入时却接受了它,下游代码就会开始在无效状态之上做出假设。
这就是 Bug 不再仅仅是“解析错误”并开始变成漏洞的地方。
## 为什么这个攻击面值得研究
DER 解析是这样一个领域:微小的验证错误可能会产生巨大的影响。
该 Bug 类别很简单:
- 长度字段说是一回事
- 实际缓冲区包含的内容却更短
- 解析器过于信任该声明
- 后续代码在不应该存在的状态上运行
这正是安全审查中值得检查的边界故障。
我在这里寻找的不是奇怪的密码学行为。
我寻找的是结构化输入处理中的信任故障。
这正是应该关注的地方。
## 根本原因
根本原因是在解析格式错误或截断的输入时,**对 DER 长度字段的验证不当**。
具体来说,`ecdsa.der.remove_octet_string()` 接受了声明的 DER 长度超过缓冲区中实际可用字节数的输入。
因此,该辅助函数没有拒绝像这样的格式错误 DER:
- 声明长度:`4096`
- 实际剩余字节数:`3`
而是接受了它,并将截断的内容作为有效内容返回。
这已经是一个 Bug 了。
但更大的影响出现在下游。
因为在边界处接受而不是拒绝了格式错误的输入,`SigningKey.from_der()` 随后可能会进入内部异常路径并抛出:
```
IndexError: index out of bounds on dimension 1
```
这很重要,因为这不是调用者期望从格式错误输入中得到的失败类型。
正确的行为是干净的解析拒绝,例如 `UnexpectedDER` 或 `ValueError`。
所以该漏洞并不仅仅是孤立存在的“IndexError”。
真正的漏洞是这样的:
- 格式错误的 DER 长度字段未得到正确验证
- 截断的输入越过了解析边界
- 下游代码因此触发了内部异常路径
这是一个完整的 Bug 链,而不是两个不相关的问题。
## 为什么这是一个安全问题,而不仅仅是糟糕的解析习惯
解析器拒绝格式错误的输入并不是表面上的改进。
它是安全模型的一部分。
这里重要的区别不在于输入是否无效。
它当然是无效的。
重要的区别在于 **面对无效输入时库的行为方式**。
以下两种情况之间存在真正的区别:
- 在边界处干净地拒绝格式错误的 DER,与
- 接受格式错误的 DER,继续深入解析,并以内部异常崩溃
前者是健壮的行为。
如果软件解析不受信任的 DER 并假设库的失败保持在预期的异常类型范围内,后者就会产生应用层面的风险。
这就是为什么它被正确地归类为漏洞,而不仅仅是解析器质量缺陷的原因。
## 概念验证
我使用了两个 PoC,因为它们演示了同一个 Bug 链的两个不同部分。
### PoC 1:截断的 DER 被接受
第一个 PoC 表明,`remove_octet_string()` 接受了声明长度超过可用缓冲区的截断 DER。
这确立了核心验证失败:
- 缓冲区比编码长度短
- 辅助函数本应拒绝它
- 但它没有
### PoC 2:确定性的内部异常路径
第二个 PoC 展示了更重要的下游影响:
在修复之前,提供给 `SigningKey.from_der()` 的格式错误 DER 会确定性地触发内部的 `IndexError`。
这确立了与安全相关的影响:
- 格式错误的输入越过了边界
- 解析走得太远
- 库代码抛出了内部异常,而不是干净的解析错误
这是一个比“解析器接受了奇怪的字节”更有力的结果。
它展示了边界故障加上实际的操作后果。
## 为什么这样选择 PoC
第一个 PoC 证明了根本原因。
第二个 PoC 证明了影响。
这种区分很重要。
许多报告停留在:
这很有用,但通常不足以说明为什么该 Bug 很重要。
在这个案例中,更有力的报告是:
- 格式错误的 DER 被错误地接受
- 这种接受并不是自我包含的
- 它可以传播到密钥解析中导致崩溃的内部异常路径
这使得安全层面的说明更加清晰。
## 修复分析
修复是微小且正确的。
该补丁添加了 `remove_sequence()` 中已使用的缺失的安全规则:
该检查被应用于:
- `remove_constructed()`
- `remove_implicit()`
- `remove_octet_string()`
一旦添加了这些边界检查,格式错误/截断的 DER 就会立即被拒绝并提示:
```
UnexpectedDER: Length longer than the provided buffer
```
并且之前触发 `IndexError` 的 PoC 不再到达内部异常路径。
它在解析期间干净地失败了,这正是从一开始就应该发生的事情。
这是你在解析器漏洞中希望看到的修复类型:
- 范围狭窄
- 明确
- 直接关联到信任边界
- 易于推理
- 有回归测试覆盖支持
没有重新设计。
没有歧义。
只是在缺失的地方进行了正确的验证。
## 回归测试
我还添加了专门的回归测试,以确保这类特定格式的错误 DER 始终被拒绝。
新测试覆盖了以下各项的截断长度拒绝:
- `remove_octet_string`
- `remove_constructed`
- `remove_implicit`
这很重要,因为该 Bug 并不是关于某个奇怪运行时路径的。
而是关于一个需要在相关 DER 辅助函数中保持一致的验证规则。
在添加了修复和测试之后,完整的测试套件在本地通过了:
```
python -m pytest -q
# 2018 个通过,5 个跳过
```
这在实际的漏洞披露工作中很重要。
当修复伴随着锁定边界的测试时,它会变得更加有力。
## 严重性和分类
此问题被合理归类为**中等**。
这里的关键影响是可用性/健壮性,而不是机密性或完整性。
咨询分类为:
- **CWE-20**:不当的输入验证
- **CVSS:** `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L`
这是合理的。
这里的主张并不是说格式错误的 DER 允许攻击者执行代码。
主张是格式错误的 DER 可能会在使用此库解析不受信任的 DER 材料的软件中触发意外的内部异常。
这是一个真实且合理的 DoS 类型解析 Bug。
## 披露
此问题是通过 GitHub Security Advisories 私下报告的。
报告包括:
- 验证 Bug
- 确定性的下游 `IndexError` 复现器
- 最小化的补丁
- 回归测试
- 本地验证结果
维护者验证了该问题,要求修复与单元测试一起提交,协调修复随后通过 GHSA 临时私有分叉工作流进行。
在处理 CVE 期间,GitHub 最初拒绝分配,因为咨询文本看起来可能描述了多个漏洞。
澄清很简单:
这是一个具有单一根本原因的**单一漏洞** —— 不当的 DER 长度验证 —— 并且 `SigningKey.from_der()` 的 `IndexError` 是这种格式错误输入被接受的下游后果,而不是一个单独可独立修复的问题。
该澄清已经足够,随后该问题被分配了:
**CVE-2026-33936**
## 这个 Bug 真正的教训
这里的教训不是“DER 很棘手”。
每个人都知道 DER 很棘手。
真正的教训是:
如果你错过了那个边界,后面的代码就会在不再可信的假设上运行。
这就是低级解析错误成为安全问题的方式。
这个 Bug 还强化了关于报告撰写和分类的一些重要事项:
- 根本原因很重要
- 影响路径很重要
- 干净地将两者连接起来更为重要
“格式错误的输入被接受”是故事的开始。
“格式错误的输入被接受,然后传播到密钥解析期间的内部异常路径”是完整的故事。
这种区分有助于清晰正确地阐明观点。
## 关键点
- DER 解析器是安全边界
- 必须根据实际缓冲区验证格式错误的长度字段
- 接受截断的结构化输入本身就是一个 Bug
- 当下游解析到达内部异常路径时,它就变成了一个更严重的漏洞
- 干净的解析拒绝是安全行为的一部分
- 最小的验证修复加上回归测试正是你在解析器 Bug 修复中所需要的
## 结语
这个漏洞与奇特的密码学无关。
它关乎的是一个解析器对格式错误输入的信任时间超出了应有的限度。
截断的 DER 长度字段越过了边界,在应该被拒绝时却通过了验证,并最终在密钥解析中导致了崩溃行为。
这就是它成为 **CVE-2026-33936** 的原因。
通过在受影响的辅助解析器中执行适当的 DER 长度边界检查进行了修复。
## 攻击链
`攻击者控制的格式错误 DER → 截断的长度被接受为有效 → 解析器越过信任边界 → SigningKey.from_der() 到达内部异常路径 → 意外的 IndexError / 应用层 DoS 风险`
## python-ecdsa 的作用
**python-ecdsa** 是一个广泛使用的 Python 椭圆曲线密码库。
除其他功能外,它还负责处理:
- 密钥解析
- 密钥序列化
- DER/ASN.1 解码
- 签名和验证工作流
这意味着其解析代码直接位于安全边界上。
每当库接受外部提供的密钥材料或结构化二进制输入时,正确性就不再仅仅是一个质量问题。
它是一项安全属性。
如果在应该拒绝格式错误输入时却接受了它,下游代码就会开始在无效状态之上做出假设。
这就是 Bug 不再仅仅是“解析错误”并开始变成漏洞的地方。
## 为什么这个攻击面值得研究
DER 解析是这样一个领域:微小的验证错误可能会产生巨大的影响。
该 Bug 类别很简单:
- 长度字段说是一回事
- 实际缓冲区包含的内容却更短
- 解析器过于信任该声明
- 后续代码在不应该存在的状态上运行
这正是安全审查中值得检查的边界故障。
我在这里寻找的不是奇怪的密码学行为。
我寻找的是结构化输入处理中的信任故障。
这正是应该关注的地方。
## 根本原因
根本原因是在解析格式错误或截断的输入时,**对 DER 长度字段的验证不当**。
具体来说,`ecdsa.der.remove_octet_string()` 接受了声明的 DER 长度超过缓冲区中实际可用字节数的输入。
因此,该辅助函数没有拒绝像这样的格式错误 DER:
- 声明长度:`4096`
- 实际剩余字节数:`3`
而是接受了它,并将截断的内容作为有效内容返回。
这已经是一个 Bug 了。
但更大的影响出现在下游。
因为在边界处接受而不是拒绝了格式错误的输入,`SigningKey.from_der()` 随后可能会进入内部异常路径并抛出:
```
IndexError: index out of bounds on dimension 1
```
这很重要,因为这不是调用者期望从格式错误输入中得到的失败类型。
正确的行为是干净的解析拒绝,例如 `UnexpectedDER` 或 `ValueError`。
所以该漏洞并不仅仅是孤立存在的“IndexError”。
真正的漏洞是这样的:
- 格式错误的 DER 长度字段未得到正确验证
- 截断的输入越过了解析边界
- 下游代码因此触发了内部异常路径
这是一个完整的 Bug 链,而不是两个不相关的问题。
## 为什么这是一个安全问题,而不仅仅是糟糕的解析习惯
解析器拒绝格式错误的输入并不是表面上的改进。
它是安全模型的一部分。
这里重要的区别不在于输入是否无效。
它当然是无效的。
重要的区别在于 **面对无效输入时库的行为方式**。
以下两种情况之间存在真正的区别:
- 在边界处干净地拒绝格式错误的 DER,与
- 接受格式错误的 DER,继续深入解析,并以内部异常崩溃
前者是健壮的行为。
如果软件解析不受信任的 DER 并假设库的失败保持在预期的异常类型范围内,后者就会产生应用层面的风险。
这就是为什么它被正确地归类为漏洞,而不仅仅是解析器质量缺陷的原因。
## 概念验证
我使用了两个 PoC,因为它们演示了同一个 Bug 链的两个不同部分。
### PoC 1:截断的 DER 被接受
第一个 PoC 表明,`remove_octet_string()` 接受了声明长度超过可用缓冲区的截断 DER。
这确立了核心验证失败:
- 缓冲区比编码长度短
- 辅助函数本应拒绝它
- 但它没有
### PoC 2:确定性的内部异常路径
第二个 PoC 展示了更重要的下游影响:
在修复之前,提供给 `SigningKey.from_der()` 的格式错误 DER 会确定性地触发内部的 `IndexError`。
这确立了与安全相关的影响:
- 格式错误的输入越过了边界
- 解析走得太远
- 库代码抛出了内部异常,而不是干净的解析错误
这是一个比“解析器接受了奇怪的字节”更有力的结果。
它展示了边界故障加上实际的操作后果。
## 为什么这样选择 PoC
第一个 PoC 证明了根本原因。
第二个 PoC 证明了影响。
这种区分很重要。
许多报告停留在:
标签:API密钥检测, ASN.1解码, CVE, CVE-2026-33936, DER解析, DoS, ECDSA, IndexError, PyPI, python-ecdsa, 密码学, 密钥解析, 手动系统调用, 拒绝服务, 数字签名, 整数下溢, 椭圆曲线密码学, 漏洞分析, 畸形数据处理, 网络安全, 路径探测, 输入验证, 边界安全, 逆向工具, 配置错误, 隐私保护