OpenZeppelin Contracts v5.7.0 ERC-7739 安全修复:拒绝 `contentsDescr` 畸形解析,杜绝签名验证退化为恒定 `structHash`

发布时间:2026/9/10 17:26:42
OpenZeppelin Contracts v5.7.0 ERC-7739 安全修复:拒绝 `contentsDescr` 畸形解析,杜绝签名验证退化为恒定 `structHash` OpenZeppelin Contracts v5.7.0 ERC-7739 安全修复拒绝contentsDescr畸形解析杜绝签名验证退化为恒定structHash【免费下载链接】openzeppelin-contractsOpenZeppelin Contracts is a library for secure smart contract development.项目地址: https://gitcode.com/GitHub_Trending/op/openzeppelin-contracts本指南以 OpenZeppelin Contracts 仓库中的 changeset 变更记录.changeset/erc7739-malformed-contents-descr.md为切入点深入讲解本次ERC7739安全修复的背景、攻击原理、修复方式与配套测试。读者将理解为什么一个看似可解析的畸形contentsDescr会把签名验证退化成对恒定structHash的校验从而让同一签名可在任意消息上重放以及本次变更如何从验证入口draft-ERC7739.sol和底层库draft-ERC7739Utils.sol两侧封堵该漏洞。变更摘要一次针对签名描述符的防御性收紧本次变更对应 changeset 文件.changeset/erc7739-malformed-contents-descr.md属于openzeppelin-solidity包的patch级别修复与 CHANGELOG.md 中 v5.7.02026-07-29条目下的ERC7739记录一致ERC7739: Reject signatures whosecontentsDescrfails to parse into a non-emptycontentsName, preventing a malformed descriptor from degrading verification to a constantstructHashthat no longer binds the message contents or the accounts EIP-712 domain.核心语义可以拆解为三层拒绝条件签名携带的contentsDescr无法解析出非空的contentsName时该签名必须被拒绝被阻止的攻击畸形描述符会让验证退化为一个恒定structHash即bytes32(0)从而不再绑定消息内容contentsHash与账户的 EIP-712 域APP_DOMAIN_SEPARATOR安全后果若不加防护攻击者可以把同一份签名用于任意消息实现跨消息的签名重放。值得注意的是这是patch级别的安全加固说明它在函数签名、公开 API 与正常用法上均无破坏性变更仅对原本应该失败但未失败的畸形输入补齐了校验。背景ERC-7739 嵌套签名如何工作在深入漏洞前需要先理解ERC7739在这个仓库中的定位。OpenZeppelin 在 contracts/utils/cryptography/signers/draft-ERC7739.sol 中提供了ERC7739抽象合约它继承自 AbstractSigner、EIP712与IERC1271用于验证把消息哈希包装进嵌套 EIP-712 类型的签名。引入这一层包装的根本目的是防重放把签名与账户自己的 EIP-712 域绑定避免同一个离链拥有者offchain owner在多个 EIP-712 域下签署的消息被互相重放。正如 draft-ERC7739.sol 注释所说明的Linking the signature to the EIP-712 domain separator is a security measure to prevent signature replay across different EIP-712 domains。isValidSignature(hash, signature)支持两种嵌套形式分别对应两种链下签名习惯Nested typed dataTypedDataSign模拟eth_signTypedData由 _isValidNestedTypedDataSignature 处理Nested personal signPersonalSign模拟personal_sign由_isValidNestedPersonalSignSignature处理。其中 TypedDataSign 路径正是本次修复的目标。嵌套签名的编码格式nested typed data 签名的编码格式由ERC7739Utils.encodeTypedDataSig定义为以下拼接结构signature ‖ APP_DOMAIN_SEPARATOR ‖ contentsHash ‖ contentsDescr ‖ uint16(contentsDescr.length)各字段含义如下字段长度含义signature可变如 65 字节的 ECDSA对嵌套 struct hash 的原始签名APP_DOMAIN_SEPARATOR32 字节请求验证的应用合约的 EIP-712 域分隔符contentsHash32 字节底层消息或数据结构的哈希contentsDescr可变对contents部分 EIP-712 类型的描述uint16(contentsDescr.length)2 字节描述符的字节长度便于定界解析在测试辅助文件 test/helpers/erc7739.js 中ERC7739Signer.signTypedData展示了链下如何构造该编码把原始签名后依次拼接TypedDataEncoder.hashDomain(domain)应用域、TypedDataEncoder.hashStruct(contentsTypeName, types, value)contentsHash、UTF-8 编码的描述符以及其 2 字节长度。漏洞机理contentsName为空如何导致退化漏洞根植于底层库 draft-ERC7739Utils.sol 中两个函数的组合行为。decodeContentsDescr畸形描述符返回空名称decodeContentsDescr 负责从描述符中解析出contentsName与contentsType。按照 ERC-7739 规范contentsName在以下情况下非法为空或包含,、)、\x00等禁止字节见_isForbiddenChar源码。该函数支持两种模式隐式模式描述符形如SomeType(address foo,uint256 bar)从开头读取contentsName显式模式描述符形如A(C c)B(A a)C(uint256 v)B类型定义在前、名称附加在后。关键行为是无论输入为空还是畸形解析失败时都返回空字符串Calldata.emptyString()。这意味着decodeContentsDescr本身无法区分空描述符和畸形描述符两者产出同样的空contentsName。typedDataSignStructHash空名称返回零哨兵typedDataSignStructHash 在计算嵌套 struct hash 前先检查contentsNamereturn bytes(contentsName).length 0 ? bytes32(0) : keccak256( abi.encodePacked(typedDataSignTypehash(contentsName, contentsType), contentsHash, domainBytes) );当contentsName为空时它直接返回bytes32(0)哨兵跳过了对contentsHash与domainBytes的哈希绑定。源码注释明确指出调用方义务Since {decodeContentsDescr} yields an emptycontentsNamefor both empty and malformed descriptors, callers must either sanitize the input so an emptycontentsNameis never passed, or reject thebytes32(0)return before signing/verifying.退化后的验证等式在修复前draft-ERC7739.sol 的_isValidNestedTypedDataSignature只检查了两点hash appSeparator.toTypedDataHash(contentsHash)以及底层原始签名验证。当contentsName为空导致typedDataSignStructHash返回bytes32(0)时实际被验证的摘要坍缩为appSeparator.toTypedDataHash(bytes32(0))这是一个只由appSeparator决定的恒定值——既不含contentsHash也不含账户自身的 EIP-712 域信息。攻击者于是可以构造这样一条被验证的摘要用它换取任意消息的通过诱导用户用底层私钥对hashTypedData(appDomain, bytes32(0))这个无害摘要签名该摘要无法通过常规signMessage/signTypedData入口产生但攻击者可以用低层signingKey.sign原语让用户签署任意摘要在签名后拼接攻击者自选的contentsHash如目标消息的哈希与畸形contentsDescr如)——非空但解析出的名称仍为空提交给验证合约由于验证退化为恒定摘要签名照常通过——同一份签名可以被无限次重放于任意内容。修复方案验证入口拒绝空contentsName修复落在验证入口_isValidNestedTypedDataSignaturedraft-ERC7739.sol上。修复后的校验链路为return hash appSeparator.toTypedDataHash(contentsHash) bytes(contentsName).length ! 0 _rawSignatureValidation( appSeparator.toTypedDataHash( ERC7739Utils.typedDataSignStructHash(contentsName, contentsType, contentsHash, _buildDomainBytes()) ), signature );注意新增的中间条件bytes(contentsName).length ! 0——这是本次 patch 的核心改动contentsName非空正常路径继续计算绑定contentsHash与账户域的嵌套 struct hash并交给_rawSignatureValidation做底层密码学校验contentsName为空无论描述符是空还是畸形验证立即短路为false不会触碰typedDataSignStructHash更不会走到底层签名校验。从更宽的视角看修复策略是双层防线底层库 draft-ERC7739Utils.sol 的typedDataSignStructHash仍保留bytes32(0)哨兵语义其文档注释也强调调用方必须拒绝该返回值或预先净化输入而验证入口在调用它之前就完成了空名称拦截因此不会发生验证退化为恒定 structHash的情况。由于改动只发生在验证函数的判定逻辑内公开 API、事件与正常输入的处理完全不变因此 changeset 标记为patch。测试佐证模拟完整攻击链路仓库测试对这次修复给出了精确到攻击步骤的验证。在 test/utils/cryptography/ERC1271.behavior.js 中测试用例 returns false for a malformed contents descriptor 完整复现了攻击流程构造恒定摘要签名使用低层signingKey.sign(hashTypedData(appDomain, ethers.ZeroHash))签署坍缩后的摘要——即appSeparator.toTypedDataHash(0)对应的摘要。测试注释特别说明常规的signMessage/signTypedData入口总会把载荷包装成结构良好的PersonalSign/TypedDataSign永远不会产生这里被利用的structHash 0摘要拼接攻击载荷选取攻击者控制的contentsHashethers.id(Message the app expects)并传入畸形描述符)——非空、但按decodeContentsDescr解析后contentsName为空断言拒绝调用isValidSignature(hashTypedData(appDomain, contentsHash), encodedSignature)必须不等于ERC-1271 魔数0x1626ba7e即签名被拒绝。该用例被shouldBehaveLikeERC1271({ erc7739: true })注入到 ECDSA、P256、RSA 三套签名算法的测试中见 ERC7739.test.js分别部署$ERC7739ECDSAMock、$ERC7739P256Mock、$ERC7739RSAMock后者定义于 ERC7739Mock.sol确保修复对三种底层密码学算法都生效。与此同时ERC7739Utils.test.js 的decodeContentsDescr测试套件系统性地覆盖了描述符解析的合法与非法输入包括合法的隐式描述符SomeType(address foo,uint256 bar)→ 解析出SomeType合法的显式描述符A(C c)B(A a)C(uint256 v)B→ 解析出名称B与类型A(C c)B(A a)C(uint256 v)空描述符、缺少(的描述符、以(开头的描述符、以及含,/)/\x00禁止字节的各种变体 → 全部返回空contentsName/contentsType。这些用例恰好印证了漏洞面非空但畸形的描述符如SomeType、(SomeType(...)、SomeType,(...)等都会产出空名称因此在修复后的ERC7739验证入口处被一律拒绝。修复验证与发布状态本次修复归属于OpenZeppelin Contracts v5.7.0CHANGELOG.md 首条版本记录2026-07-29 发布Cryptography 分类下的变更changeset 头部声明openzeppelin-solidity: patch表示它随openzeppelin/contractsnpm 包的patch版本发布属于安全加固型改动而非 API 变更。对于升级者建议按以下方式核对与验证确认修复代码存在检查 draft-ERC7739.sol 的_isValidNestedTypedDataSignature是否包含bytes(contentsName).length ! 0守卫条件当前仓库 v5.7.0 已包含运行相关测试套件在仓库根目录执行npx hardhat test test/utils/cryptography/ERC7739.test.js与npx hardhat test test/utils/cryptography/ERC7739Utils.test.js确认畸形描述符用例通过仓库使用 Hardhat见 hardhat.config.js自查集成方若你的账户/验证器在ERC7739之上自行处理描述符请确认在调用typedDataSignStructHash前对空contentsName无论是空描述符还是畸形描述符解析所致做了拒绝或净化避免再次踩中哨兵退化路径。小结这次 patch 修复揭示了一个容易被忽视的安全边界在容忍畸形输入与密码学绑定之间存在张力——decodeContentsDescr用空字符串统一表示解析失败而typedDataSignStructHash又用bytes32(0)表示空名称一旦验证入口把这两种空当作正常输入继续处理绑定就被悄然解除。修复通过在 draft-ERC7739.sol 验证入口强制要求非空contentsName让畸形描述符的签名在任何底层签名算法ECDSA、P256、RSA下都必然失败从根源上消除了恒定structHash重放攻击面。对使用 OpenZeppelinERC7739的智能合约账户与验证器而言升级到 v5.7.0 即可获得该防护无需修改任何调用代码。【免费下载链接】openzeppelin-contractsOpenZeppelin Contracts is a library for secure smart contract development.项目地址: https://gitcode.com/GitHub_Trending/op/openzeppelin-contracts创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询