【入門編】 tx.originとmsg.senderの混同によるフィッシング脆弱性 – IoT・OT(制御システム) & ブロックチェーンセキュリティ防御ガイド

こんにちは!Web3やIoTの最前線でセキュリティの研究をしているライターの私です。

新しい技術を触るのって、ワクワクしますよね。特にブロックチェーンやスマートコントラクトの世界は、自分の手で金融システムや自動化アプリを作れてしまうような面白さがあります。

でも、この世界には「現実の法律や常識とはちょっと違う、残酷なルール」が潜んでいます。その代表格が、今回お話する tx.origin と msg.sender の混同 によるフィッシングの罠です。

「難しそう……」なんて思わずに、まずは身近な「家の鍵」の防犯にたとえて、一歩ずつ紐解いていきましょう!

—

1. 家の鍵にたとえて理解する: tx.origin と msg.sender の違い

あなたが自分の家(スマートコントラクト)にセキュリティシステムを導入したと想像してください。リビングの金庫を開けるためのルールを決めなければなりません。

ここで登場するのが、Solidity(ブロックチェーンのプログラミング言語)における2つの「誰がボタンを押したか」を調べる機能です。

〇 msg.sender = 「いま目の前でインターホンを鳴らして、直接お願いしてきた人」

これは、直前にあなたと直接やり取りをしている相手のことです。もし間に「お手伝いロボット」を挟んでいるなら、そのロボットが msg.sender になります。

〇 tx.origin = 「この一連の事件のすべての元凶(元締め)」

これは、ブロックチェーンの世界で一番最初にトランザクション(取引)のボタンを押した「人間のウォレット(EOA)」のことです。間にいくらスマートコントラクトや仲介役が挟まろうとも、一番最初に財布のヒモを緩めた大元の人間を指し続けます。

なぜ tx.origin を使うのが危ないの?(泥棒の手口)

ここで、悪意のある攻撃者があなたを狙うシチュエーションを考えてみましょう。

あなたが「ちょっと面白いミニゲームだから遊んでみて!」と、攻撃者が用意した怪しいウェブサイト(コントラクトA)にアクセスし、ボタンを押したとします。
その裏で、攻撃者のサイトはあなたのウォレットを操り、あなたの資産を預けている本命の銀行コントラクト(コントラクトB)に対して「全額を引き出せ!」という命令をこっそり送りました。

このとき、銀行コントラクトBが「おや、いま命令を出してきたのは誰だ?」と tx.origin を見たとします。
すると、最初にボタンを押したのは「あなた(被害者)」なので、銀行はこう勘違いしてしまいます。
> *「おや、主人のあなた自身が直接引き出しに来たんですね。どうぞ!」*

これが、フィッシング詐欺のメカニズムです。攻撃者は、あなたを「利用(踏み台)」して、あなたになりすまして大切な資金を盗み出すのです。

—

2. 実際のコードで見てみよう:危険な書き方と正しい書き方

新人の開発者さんがよくやってしまう失敗を、コードの例で見比べてみましょう。まずは「絶対にやってはいけない危険なコード」です。

危険なパターン(tx.origin を使った送金機能)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// 【危険な例】tx.originを信用しきっているコントラクト
contract VulnerableBank {
    address public owner;

    constructor() {
        owner = msg.sender; // デプロイした人をオーナーにする
    }

    // 残高を引き出す関数
    function withdraw(address payable _to, uint256 _amount) public {
        // 🚨 致命的な脆弱性:一番最初にボタンを押した人がownerなら許可しちゃう!
        require(tx.origin == owner, "You are not the owner!");

        // 指定されたアドレスに送金する
        _to.transfer(_amount);
    }
}

このコードの何が問題か分かりますか?
もしあなたが、攻撃者が作った「すごい賞金がもらえるかも!」という悪意あるコントラクト経由で VulnerableBank の withdraw 関数を呼び出された場合、tx.origin は「あなた」になってしまうため、攻撃者はあなたの全財産を自分のウォレットに送金させることができてしまいます。

安全なパターン(msg.sender を使うべき理由)

では、どうすれば安全になるのでしょうか?答えは簡単で、tx.origin ではなく msg.sender を使って「直前の相手」を確認することです。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// 【安全な例】msg.senderで直前の呼び出し元を厳しくチェックするコントラクト
contract SecureBank {
    address public owner;

    constructor() {
        owner = msg.sender; // デプロイした人をオーナーにする
    }

    // 残高を引き出す関数
    function withdraw(address payable _to, uint256 _amount) public {
        // ✨ 安全:いま「直接」この関数を呼び出しているのがオーナー本人かを確認する
        require(msg.sender == owner, "You are not the owner!");

        // 指定されたアドレスに送金する
        _to.transfer(_amount);
    }
}

これなら、間にどんな怪しいコントラクトや仲介役が挟まろうとも、SecureBank に直接リクエストを送ってきた「直前の相手」がオーナーでなければ門前払いされます。攻撃者が仲介コントラクトを使っても、msg.sender はその「仲介コントラクトのアドレス」になってしまうため、不正な引き出しは成功しません。

原則として、コントラクトのアクセス制御(権限チェック)に tx.origin を使うのは絶対にNGと覚えておきましょう。

—

3. スマートコントラクトウォレット(ERC-4337など)の普及と新たなリスク

「なるほど、msg.sender を使えば万全ですね!」と思ったそこのあなた。さすが鋭いですが、現代のWeb3セキュリティはさらに一歩先を行っています。

最近は、従来の「秘密鍵を直接持っているウォレット(EOA)」だけでなく、機能が拡張された「スマートコントラクトウォレット(アカウント抽象化など)」を使う人が増えていますよね。

ここで重要なポイントがあります。
スマートコントラクトウォレットは、内部で別のコントラクトを呼び出してトランザクションを中継(リレー)する仕組みをよく使います。この仕組みの中では、msg.sender の値が、あなたの意図しないコントラクトのアドレスに化けてしまうことがあります。

現場のインシデントから学ぶ対策のコツ

  • アプリ間連携(コンポーザビリティ)を過信しない:

自分が書いたコードが、他の誰かの複雑なコントラクトから呼び出されることを常に想定してください。

  • 生データや署名の検証を徹底する:

単に msg.sender だけに頼るのではなく、EIP-712などの安全な署名方式を組み合わせて、「本当にこのユーザーがこの操作に同意したのか」を暗号学的に証明できるように設計するのが、プロのセキュリティリサーチャーの技の見せ所です。

—

まとめ:今日から実践できる一歩

今回は、tx.origin と msg.sender の違いと、それにまつわるフィッシングの罠について解説しました。

1. 認証や権限チェックには絶対に tx.origin を使わない!
2. 直前の呼び出し元を確認したいときは msg.sender を使う!
3. 怪しいウェブサイトや、知らないコントラクトからの「トランザクション承認(署名)」のポップアップには安易にOKを押さない!

セキュリティの世界は奥が深いですが、こうした基礎的な「落とし穴」を一つずつ知っていくことで、あなたも確実に頼れるエンジニアに近づいていきます。一歩ずつ、安全で面白いWeb3の世界を創っていきましょう!

コメント

タイトルとURLをコピーしました