【入門編】 アクセス制御の不備:初期化関数の保護 – IoT・OT(制御システム) & ブロックチェーンセキュリティ防御ガイド

こんにちは!スマートコントラクトの開発現場に飛び込んだばかりの新人エンジニアの皆さん、日々のコーディングお疲れ様です。「ブロックチェーンは改ざん不能で安全だ」とよく言われますが、実はスマートコントラクトを書く私たち人間のうっかりミスが原因で、大変なセキュリティ事故が起きてしまうことがあります。

今回は、スマートコントラクトの「アクセス制御の不備」、特にプロキシパターンにおける initialize(初期化)関数の保護漏れという、新人さんが一番最初にハマりがちな罠について、身近な例えを交えながら一歩ずつ優しく紐解いていきたいと思います。

—

家の鍵の付け忘れに似ている? プロキシパターンと初期化の仕組み

まず、ブロックチェーンの世界のちょっと特殊なルールからお話しさせてください。
イーサリアムなどのブロックチェーンに一度デプロイ(公開)したスマートコントラクトは、基本的に後からコードを書き換えることができません。「一度建てた家は、壁を壊して改築するのが大変」なイメージですね。

しかし、「やっぱり後からバグを直したり、機能を追加したりしたい!」という要望に応えるために、「プロキシパターン」という技術が使われます。これは、

  • プロキシ(代理人・玄関): ユーザーからの窓口となる契約。住所(アドレス)は変わりません。
  • ロジック(中身・部屋): 実際の処理が書かれた契約。新しい機能が必要になったら、この中身をごっそり新しいものに付け替えます。

ここで問題になるのが「初期化(お引越し)」のプロセスです。
通常のコントラクトでは、生まれたとき(デプロイ時)に一度だけ実行される constructor(コンストラクタ=新築時の初期設定)という特別な関数が使われます。しかし、プロキシパターンの場合、中身のロジック契約は後からアドレスを紐づけるため、通常のコンストラクタがうまく機能しないことがあります。

そのため、代わりに initialize という名前の「普通の関数」を作って、デプロイした後に「さあ、この変数を初期化してください!」と人間が手動で呼び出す設計にするのが一般的です。

ここで、身の回りの防犯に例えてみましょう。
新築のマンションが完成し、管理人が鍵を取り付けて「さあ、最初のオーナーに鍵を渡そう」とテーブルの上に鍵を置きっぱなしにして部屋を離れたとします。もし、その鍵に誰も鍵をかけず、部屋のドアも開けっ放しだったらどうなるでしょうか?

街を歩いていた泥棒がフラッとやってきて、勝手に合鍵を作り、「この部屋は今日から私のものでーす!」とオーナー宣言(所有権の奪取)をしてしまうかもしれませんよね。

スマートコントラクトの世界でも全く同じことが起きます。initialize 関数に「誰でも呼んでいいよ」という隙を残しておくと、悪意ある攻撃者が先回りして呼び出し、コントラクトの「オーナー(管理者)」の座を乗っ取ってしまうのです。これが、今回学ぶアクセス制御の不備の正体です。

—

危険なコードと安全なコードの比較

百聞は一見に如かず。実際にコードを見ながら、どこが危なくて、どう直すべきなのかを確認していきましょう。

1. 【危険な例】誰でもオーナーになれてしまうコード

以下のコードを見てください。一見すると普通の初期化関数に見えますが、致命的な問題が隠されています。

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

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

// 脆弱性のあるサンプル:初期化関数に保護がない
contract VulnerableVault is Initializable {
    address public owner;

    // 初期化用の関数
    function initialize() public {
        // 【危険】誰が呼んでもエラーにならない!
        // 最初にこの関数を叩いた人のアドレスがオーナーになってしまいます
        owner = msg.sender;
    }

    // お金を引き出す大切な関数
    function withdraw() public {
        require(msg.sender == owner, "Owner only");
        // 送金処理など...
    }
}

このコードの何が問題か分かりますか? initialize() 関数に public(誰からでも呼び出し可能)がついており、誰が実行してもいい状態になっています。攻撃者は、開発者がテストネットやメインネットにこのコントラクトをデプロイした瞬間を監視しており、デプロイ直後に猛烈なスピードでこの initialize() を自分からのトランザクションで呼び出します。

結果、攻撃者がコントラクトの owner になり、保管されている資金をすべて持ち去ってしまうのです。

2. 【安全な例】OpenZeppelinを使った堅牢な実装

では、どうすれば安全になるでしょうか?
業界標準であるOpenZeppelinのライブラリを使って、「初期化は一生に一度しかできない」「オーナーしか(あるいは一度だけ)呼べない」ようにガードをかけます。

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

// OpenZeppelinが提供するアップグレード・セーフティ用のライブラリをインポート
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";

// 安全なサンプル:適切なアクセス制御と初期化ガード
contract SecureVault is Initializable, OwnableUpgradeable {
    
    // コンストラクタの代わりに、ロジックコントラクト自体の初期化を防ぐ
    constructor() {
        _disableInitializers(); // これにより、中身のコントラクト単体が直接初期化されるのを防ぎます
    }

    // 初期化関数
    function initialize(address initialOwner) public initializer {
        // initializer モディファイア(魔除けのようなもの)により、この関数は「一度しか実行できない」ようになります
        
        // オーナー権限を設定する初期化処理を呼び出す
        __Ownable_init(initialOwner);
    }

    // お金を引き出す大切な関数
    function withdraw() public onlyOwner {
        // onlyOwner モディファイアにより、オーナーしか実行できない
        // 送金処理など...
    }
}

この安全なコードでは、2つの強力な防衛策が使われています。

1. _disableInitializers():ロジック契約自体がデプロイされた時点で、勝手に初期化されないようにフタをしてしまいます。
2. initializer モディファイア:initialize 関数に「この関数はコントラクトのライフサイクルの中で1回しか動かさない!」という強力な鍵をかけます。もし2回目に誰かが呼び出そうとすると、トランザクションは自動的に失敗(リバート)します。

—

実務で直面するインシデントとセキュリティチェックのコツ

私たちリサーチャーが実際の現場(監査やインシデント調査)に入るとき、真っ先にチェックするのもこの「初期化周りの設計」です。

開発に夢中になっていると、「あとでスクリプトから初期化関数を叩くから、とりあえず関数を作っておこう」と、アクセスの制限を書き忘れてしまうことが本当によくあります。特に、チーム開発で「誰が初期化スクリプトを実行するか」の共通認識がズレていると、デプロイした瞬間にボットに先回りされて全財産を失うという悲劇が起きます。

現場での対策として、以下のチェックリストを常に手元に置いておきましょう。

  • チェック1: プロキシパターンを使っている場合、constructor の代わりに initialize 関数を使っていませんか?
  • チェック2: その initialize 関数には必ず initializer モディファイア(またはそれに準ずる一度きりの実行フラグ)がついていますか?
  • チェック3: ロジックコントラクト(実装側)のコンストラクタで _disableInitializers() を呼び出して、実装コントラクト単体の乗っ取りを防いでいますか?
  • チェック4: デプロイと初期化を別々のトランザクションにせず、可能な限りデプロイメントスクリプト内で一連の流れとしてアトミックに(一瞬で)実行するように組んでいますか?

—

最後に:一歩ずつ安全な開発者へ

スマートコントラクトの世界は、一度バグを放出してしまえば、現実の銀行のように「昨日の取引を銀行員の手で巻き戻してもらう」ということが極めて難しいシビアな世界です。だからこそ、こうした細かなアクセス制御の不備に気づけるかどうかが、エンジニアとしての大きな分かれ道になります。

最初は覚えることが多くて大変に感じるかもしれませんが、一つひとつの仕組みを「なぜこの鍵が必要なんだっけ?」と身近な例えに落とし込んで理解していけば、必ず堅牢で美しいコードが書けるようになります。

焦らず、一歩ずつ安全な対策を身につけていきましょう!次回の記事でも、現場で役立つ実践的なセキュリティの知識をわかりやすく解説していきますので、ぜひお楽しみに!

コメント

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