文章详细解释了Solidity中tx.origin和msg.sender的区别,强调了tx.origin易受网络钓鱼攻击的风险,并通过一个具体的攻击场景和代码示例展示了漏洞,并提供了使用msg.sender进行访问控制的解决方案。同时,文章也指出了tx.origin的有限合法用例。
了解 tx.origin 和 msg.sender 之间的区别,何时应该使用它们,以及为什么 tx.origin 在 Solidity 中会引发网络钓鱼攻击。
本文解释了 tx.origin 和 msg.sender 在 Solidity 中的作用、它们之间的区别,以及为什么将 tx.origin 用于授权会使你的智能合约容易受到 网络钓鱼攻击 的影响。我们将通过一个具体的漏洞利用场景来展示简单的修复方法。
tx.origin 返回最初发起交易的 外部拥有账户 (EOA) 的地址。它会追溯整个调用链,直到最初的发送者。
一个有效的用例是:如果你想阻止某个特定地址与你的合约交互,tx.origin 更合适,因为该地址的所有者无法使用中介合约来规避阻止。请注意,这仅阻止单个地址。
msg.sender 是直接调用当前函数的地址。这个地址可以是 EOA 或智能合约。要识别函数的直接调用者,你需要使用全局可用的 msg 对象。
让我们用一个图示来看看它们在多合约调用链中是如何不同的:

msg.sender 始终是直接调用函数的 EOA 或智能合约,而 tx.origin 则追溯到发起整个交易的 EOA 或智能合约。
让我们看一个在 transfer 函数中使用 tx.origin 进行授权的合约:
1contract Wallet {
2 address public owner;
3
4 constructor() payable {
5 owner = msg.sender;
6 }
7
8 function transfer(address payable _to, uint _amount) public {
9 require(tx.origin == owner, "Not owner");
10
11 (bool sent, ) = _to.call{value: _amount}("");
12 require(sent, "Failed to send Ether");
13 }
14}
transfer 函数通过以下代码检查授权:
1require(tx.origin == owner, "Not owner");
正如我们在上图中看到的,tx.origin 查看的是谁发起了整个交易,而不是谁直接调用了 transfer。这意味着,如果真正的所有者发起了交易,调用链中间的恶意合约可以通过此检查。
攻击者将按以下方式利用此漏洞:
1contract Attack {
2 address payable public owner;
3 Wallet wallet;
4
5 constructor(Wallet _wallet) {
6 wallet = Wallet(_wallet);
7 owner = payable(msg.sender);
8 }
9
10 function attack() public {
11 wallet.transfer(owner, address(wallet).balance);
12 }
13}
攻击者部署此合约并指向受害者的 Wallet。然后攻击者诱骗钱包所有者调用 attack()——通过网络钓鱼邮件、恶意 dApp 链接或欺骗性交易。当所有者调用 attack() 时,它会触发 wallet.transfer()。由于 tx.origin 是钱包所有者(他们发起了交易),require 检查通过,所有资金都被转移到攻击者手中。
在你的 访问控制 检查中用 msg.sender 替换 tx.origin。修复后的 transfer 函数:
1function transfer(address payable _to, uint _amount) public {
2 require(msg.sender == owner, "Not owner");
3
4 (bool sent, ) = _to.call{value: _amount}("");
5 require(sent, "Failed to send Ether");
6}
使用 msg.sender,合约会检查谁直接调用了该函数。如果攻击者的合约调用 transfer,msg.sender 将是攻击者的合约地址——而不是钱包所有者——并且 require 将会回滚。
tx.origin 用于授权。它容易受到网络钓鱼攻击,因为它查看的是原始交易发送者,而不是直接调用者。msg.sender 进行访问控制。它能正确识别函数的直接调用者。tx.origin 的有效用例有限——主要用于阻止特定地址,以防止中介合约绕过阻止。tx.origin 检查来耗尽资金。像 tx.origin 滥用这样的授权漏洞是可以避免的,但前提是你了解在哪里查找。在 Zealynx Security,我们通过专家审计、模糊测试和有针对性的测试来帮助团队加强智能合约,这些测试可以捕获本文中介绍的漏洞。如果你正在以太坊上构建或在发布前需要安心,请联系我们或探索我们如何提供帮助。
唯一有效的用例是阻止某个特定地址与你的合约交互,因为该地址的所有者无法使用中介合约来绕过阻止。对于任何形式的授权或访问控制,请始终使用 msg.sender。
常见的攻击方式包括带有恶意 dApp 链接的网络钓鱼邮件、虚假的代币授权请求、伪装成合法交互的欺骗性交易提示,或被篡改的前端界面,将交易路由到攻击者的合约。
如果多重签名钱包使用 tx.origin 进行授权(这很不寻常),那么是的。然而,大多数多重签名钱包使用 msg.sender 检查并需要多个签名者,这使得它们本身就能抵御这种攻击。
tx.origin 与 msg.sender 的具体区别是 Solidity 和 EVM 所特有的。然而,其基本原则适用于任何地方:始终验证直接调用者,而不是原始交易发起者。Solana 和 Move 具有不同的授权模型,可以避免这种特定模式。
Slither 等静态分析工具会标记出授权检查中任何 tx.origin 的使用。在人工审查期间,审计师会查找 require(tx.origin == ...) 模式,并验证 msg.sender 是否一致地用于所有访问控制决策。
| 术语 | 定义 |
|---|---|
| 网络钓鱼攻击 | 一种社会工程学技术,攻击者诱骗受害者执行非预期操作,例如调用恶意智能合约函数。 |
| EOA | 外部拥有账户 — 由私钥控制的区块链账户,与合约账户相对。 |
| tx.origin | 一个 Solidity 全局变量,返回最初发起交易的账户地址。 |
| msg.sender | 一个 Solidity 全局变量,返回当前函数的直接调用者的地址。 |
| 访问控制 | 限制哪些地址可以调用智能合约中特定函数的安全机制,防止未经授权的操作。 |
| Solidity | 以太坊和 EVM 兼容区块链上编写智能合约的主要编程语言。 |
- 原文链接: zealynx.io/blogs/tx-orig...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!