本文深入分析了四个Web3安全漏洞:CometBFT中因验证者识别不一致导致的BFT时间戳操纵漏洞(Tachyon),Gnoswap中因Go语言值语义特性失效的重入锁,FAsset系统中因委托链接过期导致的代理金库劫持,以及Taraxa跨链桥中因授权检查依赖同交易内状态修改而导致的资产被盗事件。这些案例强调了数据验证一致性、生命周期感知访问控制和授权检查时机的重要性。
欢迎阅读《臭名昭著的Bug摘要 #7》,这是一份精心策划的关于近期Web3漏洞和安全事件的见解汇编。当我们的安全研究人员在进行审计之余,他们会花时间紧跟安全领域的最新动态,分析审计报告,并剖析链上事件。我们相信这些知识对于更广泛的安全社区具有宝贵价值,为研究人员提供磨练技能的资源,并帮助新手进入Web3安全领域。加入我们,共同探讨这批漏洞!
2026年2月2日,CometBFT(Cosmos生态系统的共识引擎)披露了一个名为“Tachyon”的严重共识层漏洞(GHSA-c32p-wcqj-j677)。该漏洞允许恶意提案者操纵区块时间戳,可能扰乱时间敏感型应用。
在CometBFT中,区块时间并非简单地由提案者的本地时钟决定。相反,它被确定性地计算为前一个区块的LastCommit中验证者提供的时间戳的加权中位数。这种机制被称为“BFT时间”,确保了恶意少数派无法改变区块链的时间。
当一个区块被提出时,其他节点通过检查LastCommit来验证它。这涉及两个不同的操作:
索引与地址:核心问题源于在这两个操作中验证者身份识别方式的不一致。
签名验证(基于索引)
为了效率,签名验证依赖于验证者在验证者集合中的索引。在有漏洞的代码中,验证逻辑通过索引获取验证者,但没有验证CommitSig中的ValidatorAddress字段是否与验证者匹配。签名覆盖了BlockID、Height、Round和Timestamp,但不包括ValidatorAddress字段。
时间计算(基于地址)
相反,用于计算区块时间的MedianTime函数通过提交的ValidatorAddress查找验证者。如果给定地址未找到验证者,代码会静默跳过该签名,而不是返回错误。
攻击者(一个恶意验证者)可以提出一个带有有效签名的区块,并将ValidatorAddress替换为虚假地址。签名验证(使用索引)通过,但时间计算(使用地址)未能找到验证者并静默跳过。通过选择性地“静音”特定验证者,攻击者可以使加权中位数计算偏向其选择的值。
为了修复此问题,CometBFT v0.38.21在签名验证期间引入了严格的地址验证(确保索引与地址匹配),并更新了MedianTime以在缺少验证者时返回错误,而不是静默跳过。
这突出表明数据验证中的一致性至关重要。当多个子系统(验证与计算)依赖相同的数据结构时,它们必须使用相同的识别方法和验证严格性。此外,静默失败(例如在没有错误的情况下跳过缺失的验证者)通常隐藏了可能被利用的逻辑错误。
请参阅以下文章,了解可能的利用向量概述:Tachyon: Saving $600M from a Time-Warp Attack。
此漏洞是在OpenZeppelin对Gnoswap进行审计时发现的。Gnoswap是一个基于Gno(一种Go派生的智能合约语言)的去中心化交易所。与Uniswap V3类似,Gnoswap的Swap函数使用Slot0结构体中的布尔型unlocked标志来防止重入。预期的模式是:读取Slot0,在任何外部回调之前将unlocked设置为false,执行交换,并通过defer将unlocked恢复为true。任何重入调用都会看到该标志并触发panic。
池通过返回结构体值的getter暴露Slot0:
type Slot0 struct {
...
unlocked bool
}
func (s *Slot0) SetUnlocked(unlocked bool) {
s.unlocked = unlocked
}
func (p *Pool) Slot0() Slot0 {
return p.slot0
}
在Go(和Gno)中,按值返回结构体意味着调用者收到一个独立的副本。Swap函数以以下方式使用此getter:
func (p *Pool) Swap(...) {
slot0Start := p.Slot0() // returns a copy
if !slot0Start.Unlocked() { // This will always be true
panic("LOCKED")
}
slot0Start.SetUnlocked(false) // modifies the copy
defer slot0Start.SetUnlocked(true) // modifies the copy
...
}
由于pool.Slot0()返回一个值,slot0Start是一个独立的副本。因此,调用slot0Start.SetUnlocked(false)只修改此局部变量,而池的实际slot0.unlocked字段保持为true。重入保护从未生效。任何重入调用进入Swap都会读取pool.Slot0().Unlocked(),看到true,并顺利通过检查。
在保护被禁用后,swapCallback成为一个无保护的入口点。回调返回后,safeSwapCallback会检查池的代币余额是否按预期增加,作为安全网。然而,由于重入是可能的,攻击者可以在回调中调用其他池或头寸函数(例如IncreaseLiquidity),将代币存入池中。这些存款会使余额膨胀,导致回调后的检查通过,即使攻击者没有进行预期的交换还款,从而有效地从池中窃取价值。
在像Go和Gno这样具有值语义的语言中,依赖于突变的模式(如重入保护)可能会静默失败。当getter按值返回结构体时,对返回副本的任何突变都会丢失,除非明确写回。审计Go或Gno智能合约的审计师应密切关注状态修改操作是作用于实际存储状态还是作用于临时副本。
此漏洞是在OpenZeppelin对Flare FAssets进行审计时发现的。
FAsset系统是Flare网络的核心组件,它允许将来自其他链(例如BTC、XRP)的资产以FAssets的形式信任地发行到Flare上。代理(Agent)将原生抵押品(如FLR或SGB)锁定在代理金库(Agent Vaults)中,并以此赚取费用。
AgentOwnerRegistry合约支持两层权限模型以实现操作安全:
两个映射实现了这一机制:workToMgmtAddress(工作地址 → 管理地址)和mgmtToWorkAddress(管理地址 → 工作地址)。一个白名单代理通过setWorkAddress函数链接一个工作地址。重要的是,该函数阻止已在白名单中的地址被设置为工作地址。否则,在金库创建时使用的_getManagementAddress会将工作地址解析为其管理器,并错误地归属金库所有权。规则是:一个白名单代理必须始终被视为顶级所有者,而不是下属。
该漏洞是一个生命周期疏忽:setWorkAddress只在委托时强制执行“不允许白名单工作地址”的规则。当一个地址的状态稍后改变时,协议不会重新验证或清除现有链接。如果一个已经注册为工作地址的地址稍后被治理白名单,旧的委托就会保留。这个陈旧的workToMgmtAddress条目随后可以被滥用以劫持新代理的金库。
故障链始于whitelistAndDescribeAgent。这个治理函数添加新代理,但不会检查候选地址是否已出现在workToMgmtAddress中。白名单过程在不清除或使任何现有工作地址链接失效的情况下进行,因此预先存在的委托保持活跃。
当受害者(现在已在白名单中)调用createAgentVault时,合约使用_getManagementAddress来解析所有权。该辅助函数信任workToMgmtAddress。由于受害者之前被绑定为工作地址,他们被解析为攻击者的管理地址。因此,金库在攻击者的控制下创建,并通过白名单检查。
利用此漏洞的攻击将按以下步骤进行:
setWorkAddress(B)来抢先治理的即将进行的B的白名单操作,确保在A调用时B是非白名单的,同时锁定工作地址→管理地址的链接。workToMgmtAddress条目。旧的映射仍然存在。教训是访问控制检查必须是生命周期感知的。仅在单个时间点(例如,在委托时)验证状态是不够的,因为实体随后可以改变角色(例如,从工作地址变为白名单代理)。因此,安全性必须考虑状态转换,并重新验证或使依赖状态失效。
修复方法是为工作地址委托引入明确的选择加入:被提名的工作地址必须接受链接才能使其生效。这消除了未经同意绑定地址的能力,并关闭了劫持向量,同时保持了白名单和委托的可用性。
Taraxa Bridge智能合约(用于在以太坊和Taraxa链之间进行资产转移)于2026年2月22日被利用,导致约1.3万美元的ETH、USDT和TARA代币损失。该漏洞是由于授权检查不当造成的,该检查依赖于同一交易中较早发生的状态变异,从而使攻击者能够颠覆基于验证者的法定阈值。
桥接架构依赖于TaraClient合约在以太坊上完成Taraxa区块。此过程涉及验证验证者签名,确保累积验证者权重达到预定义的weightThreshold,然后通过finalizeBlocks函数将批准的桥接根存储在finalizedBridgeRoots映射中。Bridge合约随后通过applyState函数使用这些已完成的根,以在以太坊上执行相应的状态更改,例如释放桥接资产。
该漏洞源于TaraClient在验证验证者法定人数是否达到所需weightThreshold之前,通过processValidatorChanges应用了验证者更新。具体来说,合约使用在尝试区块完成的同一交易中提供的calldata更新了验证者的voteCount和totalWeights。这些新变异的权重随后立即用于授权检查。
通过精心构造恶意验证者更新,攻击者人为地夸大了其控制下的验证者的权重。由于法定人数检查是针对此修改后的状态执行的,攻击者能够在不拥有合法验证者共识的情况下满足weightThreshold。这使得攻击者能够完成一个恶意桥接根并将其存储在finalizedBridgeRoots中。
由于欺诈性桥接根现在被认为是有效的,攻击者调用了Bridge.applyState,该函数信任该根并执行了编码的状态转换。结果,桥接释放了其托管资产,将ETH、USDT和TARA代币直接转移给攻击者。
此事件强调了一个关键的智能合约设计缺陷:授权和共识检查绝不能依赖于在同一调用上下文中较早修改的状态。验证者集合更新和法定人数验证必须严格分离或分阶段跨交易进行,以防止攻击者在执行过程中操纵信任假设。
- 原文链接: x.com/openzeppelin/statu...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!