【実務・中級編】 未初期化プロキシコントラクトの脆弱性 – IoT・OT(制御システム) & ブロックチェーンセキュリティ防御ガイド

制御不能なアップグレード:未初期化プロキシの「乗っ取り」を防ぐ極意

現場のエンジニア諸君、お疲れ様。今日はスマートコントラクトのセキュリティにおいて、初心者が陥りやすく、かつ被害が壊滅的になりがちな「未初期化プロキシコントラクト(Uninitialized Proxy Contract)」の脆弱性について話そう。

SCADAやIoTのバックエンドでブロックチェーンを活用する際、アップグレード可能な設計(Proxyパターン)はもはや標準だ。しかし、この「便利さ」は、設計ミスひとつで全資産の消失や、管理者権限の強奪に直結する。なぜなら、プロキシの初期化関数を誰でも呼べる状態に放置することは、家の鍵が開いたまま新築の家に誰でも入れる状態にするのと同じだからだ。

なぜ「コンストラクタ」が使えないのか?

まず、大前提を整理する。EVM(Ethereum Virtual Machine)において、アップグレード可能なコントラクト(Logicコントラクト)は、constructorを直接使えない。なぜなら、プロキシパターンではロジックコントラクトのコードがプロキシ側でdelegatecallによって実行されるため、ロジックコントラクト自身のコンストラクタはデプロイ時にしか実行されず、プロキシ側のストレージは初期化されないからだ。

そこで、多くのエンジニアはinitialize()という名前の関数を定義し、それをデプロイ直後に叩く実装を選ぶ。だが、ここで「アクセス制限」を忘れるとどうなるか。答えは明白だ。攻撃者がデプロイ直後のトランザクションを見張り、先にその関数を叩いて「管理者」の座を奪い取る。

PoC:攻撃者はどうやってコントラクトを乗っ取るか

もし、あなたの実装が以下のような状態なら、即座に修正が必要だ。

// 脆弱な実装例:修飾子がないため誰でも初期化可能
function initialize(address _admin) public {
    admin = _admin; // 悪意のあるアドレスを指定されてしまう
}

攻撃者は、コントラクトがブロックチェーン上に刻まれた瞬間、initialize(attacker_address) を呼び出す。これだけで、コントラクトの管理権限(owner)は攻撃者の手に渡り、資産の引き出しやロジックの書き換えが自由に行われる。

【鉄則】セキュアな実装サンプル:initializer修飾子の活用

OpenZeppelinのライブラリを使っているなら、車輪の再発明はするな。Initializableを継承し、initializer修飾子を付けるのが今の「正解」だ。

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

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

contract SecureVault is Initializable {
    address public admin;

    /// @dev 初期化関数には必ず initializer 修飾子を付与する
    /// これにより、一度しか実行できないことが担保される
    function initialize(address _admin) public initializer {
        require(_admin != address(0), "Invalid address");
        admin = _admin;
    }

    // アップグレード時に初期化が必要な場合は reinitializer(version) を使う
}

実務で「絶対」に守るべきチェックリスト

コードをコミットする前に、以下の3点を必ず確認してほしい。

1. initializer修飾子の有無: 初期化関数にこれが付いていない場合、それはバグではなく「セキュリティホール」だ。
2. constructorでの初期化: ロジックコントラクトのconstructor内で _disableInitializers(); を呼べ。これにより、ロジックコントラクト自体が直接初期化されるのを防げる。これはプロキシパターンの必須作法だ。
3. デプロイ・スクリプトの検証: デプロイと初期化を分離させず、単一のトランザクション、あるいはスクリプト内で確実に実行するようにCI/CDを組む。

修正後のconstructor実装(推奨)

abstract contract SecureLogic is Initializable {
    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        // ロジックコントラクト自体を初期化不能にする
        _disableInitializers();
    }
}

最後に:セキュリティは「性悪説」で設計せよ

現場でインシデントに接するたびに思うのは、多くのエンジニアが「誰もこんな面倒な攻撃はしないだろう」という甘い期待を持っていることだ。だが、オンチェーン上のボットは数ミリ秒単位でデプロイを監視し、脆弱性を機械的に突き続けている。

今回紹介したinitializerは、ほんの数行の修正だ。しかし、この数行を怠るだけで、数億円規模の損失を出すリスクを背負うことになる。

もし君が今、既存のプロキシコントラクトを運用しているなら、すぐにinitialize関数が公開(public)状態のままになっていないか確認してほしい。もし修正が必要な場合は、即座にアップグレードを実施すること。セキュリティは「後回し」が一番の毒になる。

次回の講義では、プロキシコントラクトの「ストレージ競合(Storage Collision)」について深掘りする。これもまた、現場でよく見る「動いているけど実は爆弾」の典型例だ。準備しておいてくれ。

コメント

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