高危权限控制静态扫描有 1 条规则
tx.origin 钓鱼
tx.origin Authentication
用 tx.origin 判断调用者身份,攻击者就能诱骗受害者调用自己的恶意合约,由恶意合约去「冒充」受害者通过校验。因为 tx.origin 始终是最初发起交易的那个 EOA,整条调用链都骗不过它。
根因
把 tx.origin(整笔交易的发起人,永远是 EOA)当成了 msg.sender(当前这一跳的调用者)。前者跨越整个调用链不变,后者每跳都会变。用不变的那个做权限,等于把权限判给了「整条链的起点」,而中间任何一环都可以被攻击者控制。
攻击是怎么一步步发生的
- 1攻击者
部署恶意合约和伪装页面(例如「免费领取代币」)
- 2受害者
在页面上点「领取」,签名并发出交易 —— tx.origin = 受害者
- 3恶意合约
claimAirdrop() 被调用,msg.sender = 受害者,tx.origin = 受害者
- 4受害者的钱包合约
tx.origin == owner成立,校验通过 - 5受害者的钱包合约
把余额转给「恶意合约里的 msg.sender」= 攻击者
有漏洞的版本
不要照抄soliditySolidity29 行
contract TxOriginWallet {
address public owner;
constructor() {
owner = msg.sender;
}
// 危险:tx.origin 在整条调用链上都是最初那个 EOA
function transfer(address payable to, uint256 amount) external {
require(tx.origin == owner, "not owner");
to.transfer(amount);
}
receive() external payable {}
}
contract Attacker {
TxOriginWallet private immutable wallet;
constructor(address walletAddr) {
wallet = TxOriginWallet(payable(walletAddr));
}
// 受害者只要调用这个函数(比如被伪装成「领取空投」),
// tx.origin 就是受害者本人,于是钱包里的钱被转到攻击者手上
function claimAirdrop() external {
wallet.transfer(payable(msg.sender), address(wallet).balance);
}
}tx.origin == owner在攻击者合约调用时依然成立,因为发起交易的还是受害者- 攻击的核心是「诱导受害者主动发起一笔交易」,所以这本质上是一次钓鱼
- 用户界面无法提示这种风险:钱包看到的是「调用某个合约」,看不出会转账
修复版本
推荐写法soliditySolidity15 行
contract SafeWallet {
address public owner;
constructor() {
owner = msg.sender;
}
// msg.sender 是「当前这一跳的调用者」。
// 攻击者合约调用时,msg.sender 是攻击者合约,不等于 owner,校验失败。
function transfer(address payable to, uint256 amount) external {
require(msg.sender == owner, "not owner");
require(to != address(0), "zero address");
to.transfer(amount);
}
}- 把
tx.origin一律换成msg.sender。这一条没有例外 - 唯一合理的 tx.origin 用法是「防止合约调用」这种反模式,但那也不该用它实现
- 如果确实需要「EOA 才能调用」,用
require(msg.sender == tx.origin)做辅助判断,但权限必须是 msg.sender
怎么防
- 权限判断一律用 msg.sender,永远不用 tx.origin
- 代码审查时全局搜索
tx.origin,每一处都要解释清楚为什么在这里是安全的 - 对用户侧:不要对来源不明的「领取」页面签名或发交易
真实案例
2018多个以太坊钱包合约累计数百万美元级别
tx.origin 钓鱼长期是钱包类合约的常见失陷方式
审计这个合约时要问的问题
- 1.代码里有 tx.origin 吗?每一处为什么需要它?
- 2.权限校验能不能被「中间合约」绕过?
- 3.有没有「用户主动调用某个合约却导致自己资产被转走」的路径?
这些问题也是审计检查清单里对应的条目——那里可以把每一条的检查结果记下来,最后导出成报告。
静态扫描能发现它吗
能——扫描引擎里有 1 条规则与它相关:
tx-origin但「命中规则」只说明代码里出现了相关的写法,不说明它一定有问题、也不说明没有它就没问题。命中了就回到源码确认调用路径。