跨链桥安全研究:OP Stack CrossDomainMessenger 重放漏洞
一次对 Optimism OP Stack 跨链消息机制的审计记录。
什么是 CrossDomainMessenger
OP Stack 的跨链桥基于 CrossDomainMessenger 合约,它负责在 L1(以太坊主网)和 L2(OP 链)之间传递消息。
简单来说,它的工作流程是:
- L1 上的合约调用
sendMessage() - CrossDomainMessenger 把消息塞进队列
- 中继者(relayer)在目标链上调用
relayMessage() - 目标合约收到消息并执行
漏洞:V0 版本缺少重放保护
我分析的是 V0(初始版本)的 relayMessage() 函数。问题出在一个关键细节——消息的 nonce 设计存在缺陷。
function relayMessage(
address _target,
uint256 _nonce,
bytes memory _message
) external {
// 检查消息是否已被执行
bytes32 messageHash = keccak256(abi.encode(_target, _nonce, _message));
require(!executedMessages[messageHash], "already relayed");
// 执行消息
(bool success, ) = _target.call(_message);
require(success, "relay failed");
executedMessages[messageHash] = true;
}
问题分析
_nonce 参数由调用者(relayer)提供。如果在消息中包含了可变数据(比如 blockhash、timestamp),messageHash 会不同,可以重复提交不同的 _nonce 值来重放同一笔 _target.call(_message)。
更关键的场景是:如果同一笔跨链消息在多个链/多个分叉上都被中继,只要 nonce 不同,消息就能被执行多次。而消息的 _target 和 _message 数据完全一致,意味着目标合约会被调用多次——这在代币转账场景下就是双花。
影响
在 OP Stack V0 中,这个漏洞被评为 Medium。因为利用条件相对严格:
- 需要主动构造不同的 nonce
- 需要控制或影响中继者
- 部分跨链桥在更高版本中已修复
但原理本身非常经典——跨链桥安全的核心问题永远是如何保证"一笔消息只被执行一次"。
其他跨链桥类似问题
这不是孤例。历史上多个跨链桥都出过类似问题:
- Wormhole — 验证器签名验证缺失(3.26 亿美元被盗)
- Nomad — 中继者信任假设被打破(1.9 亿美元被盗)
- Multichain — 私钥泄露导致桥资金被盗(12.6 亿美元)
我的发现思路
这个漏洞是如何找到的?说白了就是"怀疑一切"的心态:
- 拿到 OP Stack 合约源码
- 从最核心的
relayMessage()入手 - 关注所有由调用者提供的参数——它们都是潜在的"攻击面"
- 检查状态变量的锁定逻辑——
executedMessages能否被绕过 - 测试 nonce 重用场景
挖合约漏洞的门槛其实不高,难的是思维方式——你得像一个攻击者那样读代码。
后续
这个发现提交到了 Cantina,被评级为 Medium,目前正在等待审核结果。
不管结果如何,这次审计让我对跨链安全有了更深的理解。跨链桥是 DeFi 里最脆弱的一环——攻击面大、责任集中、历史劣迹斑斑。如果以后继续深入安全领域,跨链一定是我重点跟的方向。
