要約
ユーザ認証にtx.originを使ってはいけませんというお話。
バグのあるウォレット
fallback関数による入金機能と、transferTo関数による送金機能、それとgetBalance関数による残高確認機能のみのシンプルなスマートコントラクトウォレットを作りました。
pragma solidity ^0.4.11;
// THIS CONTRACT CONTAINS A BUG - DO NOT USE
contract MyWallet {
address owner;
function MyWallet() public {
owner = msg.sender;
}
function() public payable {}
function transferTo(address _dest, uint _amount) public {
require(tx.origin == owner); // BAD
_dest.call.value(_amount)();
}
function getBalance() public view returns (uint) {
return this.balance;
}
}
transferTo関数ではrequireでこのコントラクトをインストールした自分以外の呼び出しはエラーになるようにしてあります。誰かに残高を盗まれたら困りますからね。
そして1ETHを入金しておきました。getBalance関数を呼ぶと1000000000000000000が返ってきます。
送金
なにか物を買うかして、アドレス0x0123456789abcdef0123456789abcdef01234567に10weiを送金することになりました。
transferTo(0x0123456789abcdef0123456789abcdef01234567, 10)
を実行すれば良いです。簡単ですね。
残高確認
getBalance関数を呼ぶと0が返ってきました??????
攻撃ウォレット
実はアドレス0x0123456789abcdef0123456789abcdef01234567はEOAではなくコントラクトでした。
pragma solidity ^0.4.11;
interface Wallet {
function transferTo(address _dest, uint _amount) public;
}
contract AttackWallet {
address owner;
function AttackWallet() public {
owner = msg.sender;
}
function() public payable {
Wallet(msg.sender).transferTo(owner, msg.sender.balance);
}
}
MyWalletからAttackWalletへ送金をすると、AttackWalletのfallback関数が呼ばれます。AttackWalletのfallback関数では、再度MyWalletのtransferTo関数を呼んでいます。transferTo関数で呼び出し元をチェックしているから大丈夫… と思いきや、チェックしているのはtransferTo関数の呼び出し元ではなく、**tx.origin(トランザクションの呼び出し元)**です。トランザクションの呼び出し元は最初にtransferTo関数を呼んだEOAアカウント、つまりMyWalletのownerなので、requireは成立してしまい、MyWalletの全ての残高はAttackWalletのownerに送金されてしまいました。
どうすれば良かったのか
この部分
require(tx.origin == owner); // BAD
呼び出し元チェックはmsg.senderを使いましょう
require(msg.sender == owner); // GOOD
また、 https://qiita.com/k-keisuke/items/cd2744c7ba085beff16b にあるように、送金時はaddress.call.value()()ではなく、addess.transfer()を使いましょう。後者はfallback関数で2300gasを超える処理を実行するとエラーになります。
参考
http://solidity.readthedocs.io/en/develop/security-considerations.html#tx-origin
ここのコードを若干改変
https://medium.com/coinmonks/solidity-tx-origin-attacks-58211ad95514
解説はここを参考