W3
高危权限控制静态扫描有 1 条规则

tx.origin 钓鱼

tx.origin Authentication

用 tx.origin 判断调用者身份,攻击者就能诱骗受害者调用自己的恶意合约,由恶意合约去「冒充」受害者通过校验。因为 tx.origin 始终是最初发起交易的那个 EOA,整条调用链都骗不过它。

根因

tx.origin(整笔交易的发起人,永远是 EOA)当成了 msg.sender(当前这一跳的调用者)。前者跨越整个调用链不变,后者每跳都会变。用不变的那个做权限,等于把权限判给了「整条链的起点」,而中间任何一环都可以被攻击者控制。

攻击是怎么一步步发生的

  1. 1
    攻击者

    部署恶意合约和伪装页面(例如「免费领取代币」)

  2. 2
    受害者

    在页面上点「领取」,签名并发出交易 —— tx.origin = 受害者

  3. 3
    恶意合约

    claimAirdrop() 被调用,msg.sender = 受害者,tx.origin = 受害者

  4. 4
    受害者的钱包合约

    tx.origin == owner 成立,校验通过

  5. 5
    受害者的钱包合约

    把余额转给「恶意合约里的 msg.sender」= 攻击者

有漏洞的版本

不要照抄solidity
Solidity29
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 在攻击者合约调用时依然成立,因为发起交易的还是受害者
  • 攻击的核心是「诱导受害者主动发起一笔交易」,所以这本质上是一次钓鱼
  • 用户界面无法提示这种风险:钱包看到的是「调用某个合约」,看不出会转账

修复版本

推荐写法solidity
Solidity15
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

但「命中规则」只说明代码里出现了相关的写法,不说明它一定有问题、也不说明没有它就没问题。命中了就回到源码确认调用路径。

延伸阅读

同属「权限控制」的其它 1

下一步:想知道这条在手上的合约里有没有出现,用Rug Pull 扫描器,它会检查前面列出的 1 条相关规则。 审计检查清单可以把它落成一条可勾选、可写备注的记录,最后导出 Markdown 报告;想先弄懂这一类漏洞背后的通用思路,去参考手册看对应的语言与标准条目。