【入門編】 未初期化プロキシコントラクトの乗っ取り – IoT・OT(制御システム) & ブロックチェーンセキュリティ防御ガイド

こんにちは!Web3の世界へ飛び込んだばかりの新人開発者の皆さん、スマートコントラクトの開発、日々の学習お疲れ様です。

ブロックチェーンの世界では、「コードは法律(Code is Law)」と言われますよね。一度デプロイされたスマートコントラクトは、誰も書き換えることができない……というのが基本です。しかし、近年のスマートコントラクトでは、後から機能をアップデートできるように「プロキシパターン」という仕組みがよく使われています。

今回は、このプロキシパターンに潜む、新人泣かせの恐ろしい罠「未初期化プロキシコントラクトの乗っ取り」について、身近な例えを交えながら優しく紐解いていきたいと思います。一歩ずつ、しっかりと対策を学んでいきましょう!

—

1. 「プロキシパターン」ってなに? 家の鍵でたとえてみよう

スマートコントラクトは一度ブロックチェーンに書き込むと修正ができません。「あ、バグがあった!」と思っても、そのままでは直せないのです。これでは困るので、頭の良いプログラマーたちは「プロキシ(代理人)パターン」という仕組みを考え出しました。

これを私たちの生活にたとえてみましょう。

  • プロキシコントラクト(玄関のドア・鍵): ユーザーが最初にアクセスする窓口です。住所(コントラクトアドレス)は変わりません。
  • ロジックコントラクト(中身の家具や家電): 実際に計算をしたり、お金を動かしたりする本当のプログラムです。

あなたが新しい家(プロキシ)を建てたとします。玄関の鍵(アドレス)は一つですが、中の家具や内装(ロジック)は、必要に応じてリフォーム業者(開発者)がガラッと入れ替えることができます。これなら、バグが見つかっても中身だけを新しいものに交換できますよね。

とっても便利な仕組みなのですが、ここに「新築なのに鍵が開きっぱなしだった」という致命的なうっかりミスが起きることがあるのです。それが今回のテーマです。

—

2. なぜ乗っ取られるの? 攻撃のメカニズム

コントラクトの世界では、通常のコンストラクタ(初期化関数)の代わりに、デプロイ後に呼び出す initialize のような専用の関数を使って「私がこのコントラクトのオーナー(管理者)です!」と名乗り出ます。

ここで想像してみてください。
あなたが新しいマンションを建てて、ピカピカの玄関ドア(プロキシ)を取り付けました。中の部屋(ロジック)も準備OKです。しかし、「あなたが鍵をかけて、自分の名前を管理台帳に書き込む(初期化する)」という最初の儀式をうっかり忘れて、そのまま出かけてしまったとしたら……?

そこに通りかかった泥棒(攻撃者)がどうするか、もう分かりますよね。

1. 泥棒が、誰も鍵をかけていないあなたの部屋の管理台帳を見つけます。
2. 泥棒は真っ先に自分の名前を「オーナー」として書き込みます。
3. 一度オーナーになってしまえば、泥棒はこの部屋の合鍵を勝手に作ったり、金庫の中身を全部持ち出したりやりたい放題です。

これが、「未初期化プロキシコントラクトの乗っ取り」の正体です。ロジックコントラクト側で初期化関数が実行されていなくても、プロキシ側で初期化の処理が行われていないと、誰でも最初にその関数を呼べてしまうのです。

—

3. コードで見てみよう:危ない実装と安全な実装

百聞は一見にしかず。実際にどのようなコードが危険で、どう直すべきなのかを見ていきましょう。

危ないコードの例(初期化を忘れたロジック)

まずは、うっかり初期化関数(initialize)を防衛していなかったり、デプロイ時にプロキシから即座に呼び出さなかったりするケースです。

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

// 【危険な例】ロジックコントラクト
contract VulnerableVault {
    address public owner;

    // 初期化関数
    function initialize() public {
        // ⚠️【盲点】ここで「すでにオーナーがいないか」をチェックしていない!
        owner = msg.sender;
    }

    function withdrawAll() public {
        require(msg.sender == owner, "Aあなたはお金を引き出す権限がありません!");
        payable(msg.sender).transfer(address(this).balance);
    }
}

このコントラクト単体では普通に見えますが、プロキシ経由で使われる際、デプロイ直後に誰もこの initialize() を呼ばないと、一番最初にこの関数をトランザクションで叩いた悪意あるユーザーが、一瞬で owner になってしまいます。

—

安全なコードの例(自動初期化と防御策)

では、どうすれば泥棒を防げるでしょうか? 実務では、プロキシをデプロイするスクリプト(JavaScript/TypeScript等)の中で、デプロイと初期化をワンセット(自動)で行うのが鉄則です。

以下は、Hardhatなどの開発環境でよく使われるデプロイメントスクリプトの例です。

// Hardhat / ethers.js を使った安全なデプロイと初期化のサンプル
const { ethers } = require("hardhat");

async function main() {
    const [deployer] = await ethers.getSigners();
    console.log("デプロイを実行するアカウント:", deployer.address);

    // 1. ロジックコントラクトをデプロイ
    const VulnerableVault = await ethers.getContractFactory("VulnerableVault");
    const logic = await VulnerableVault.deploy();
    await logic.waitForDeployment();
    console.log("ロジックコントラクトのアドレス:", await logic.getAddress());

    // 2. プロキシコントラクトをデプロイし、同時に初期化関数(initialize)を呼び出す!
    // ※ ここでデータを渡して実行することで、デプロイした瞬間に鍵をかけます。
    const ERC1967Proxy = await ethers.getContractFactory("ERC1967Proxy");
    
    // initialize関数のバイナリデータをエンコードする
    const initData = logic.interface.encodeFunctionData("initialize", []);

    const proxy = await ERC1967Proxy.deploy(
        await logic.getAddress(), 
        initData // ←【重要】ここで即座に初期化を実行し、オーナーを自分に固定する
    );
    await proxy.waitForDeployment();

    console.log("安全にデプロイされたプロキシのアドレス:", await proxy.getAddress());
}

main().catch((error) => {
    console.error(error);
    process.exitCode = 1;
});

さらに、ロジックコントラクト側でも _disableInitializers(); というOpenZeppelin社が提供する便利な仕組みを使い、ロジックコントラクト自体が直接初期化されないようにフタをしておくのが現代のスタンダードです。

—

4. 現場のインシデントから学ぶ:防犯の心構え

私たちが現場でスマートコントラクトのセキュリティ監査(コードレビュー)を行う際、プロキシ関連の脆弱性は最も緊張するポイントの一つです。「動けばいいや」と手動でデプロイのステップを分けてしまうと、人間は必ずうっかりミス(ヒューマンエラー)を起こします。

現場のインシデントを防ぐために、次の鉄則を心に刻んでおきましょう。

1. デプロイと初期化は必ず「自動化」する
人間が手動でコンソールから initialize を叩く運用は絶対に避けましょう。デプロイメントスクリプトを使い、1つのトランザクション、あるいは一連のプログラムフローの中で確実に初期化を完了させます。
2. OpenZeppelinのアップグレードプラグインを活用する
車輪の再発明は禁物です。実績のある信頼性の高いライブラリ(OpenZeppelin Contracts Upgradeableなど)を使い、初期化状態の管理をフレームワークに任せましょう。
3. テストネットで十分にリハーサルを行う
本番環境へデプロイする前に、必ずテストネット(Sepoliaなど)でデプロイ手順を丸ごとテストし、誰がオーナーになっているかをEtherscan等のエクスプローラーで自分の目で確認する習慣をつけましょう。

—

まとめ

今回は、未初期化プロキシコントラクトの乗っ取りについて、家の鍵のたとえを交えながら解説しました。

  • プロキシパターンは便利だけど、最初の「鍵かけ(初期化)」を忘れると泥棒に入られる。
  • 対策として、デプロイと初期化はスクリプトで自動化し、隙を作らないことが大切。

セキュリティの世界は覚えることが多くて最初は圧倒されてしまうかもしれませんが、一つひとつの仕組みを「身近な例え」に置き換えて理解していけば、必ず確実なスキルが身についていきます。焦らず、一歩ずつ安全なコードを書けるエンジニアを目指していきましょう!

コメント

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