「家の鍵を誰でも複製できる状態」になっていませんか?スマートコントラクトのアップグレード権限を守る話
こんにちは!Web3セキュリティの世界へようこそ。
今日は、スマートコントラクト界隈で時折ニュースを賑わせる「乗っ取り」の正体について、少し深掘りしてみましょう。
開発者の皆さんなら「コントラクトを修正したい」と思ったことはありませんか?でも、ブロックチェーンは「書き換え不可」が基本。そこで登場するのがProxy(プロキシ)パターンという魔法の仕組みです。
しかし、この魔法、実は使い方を間違えると「泥棒に家の鍵を丸ごと渡す」のと同じくらい危険な落とし穴があるんです。今日は、新人のIT担当者の方にもわかるように、この「アップグレードの罠」を紐解いていきましょう。
—
1. Proxyパターンって何?:家の「玄関」と「中身」を分ける考え方
Proxyパターンを一言で言うと、「家の玄関(Proxy)と、中の家具(Logic)を分けて管理する仕組み」のことです。
- Proxy(玄関): ユーザーがアクセスする固定の住所。
- Logic(家具): 実際の機能が詰まった中身。アップグレードが必要な時は、この「家具」だけを新しいものに入れ替えます。
こうすることで、ユーザーは「玄関の場所」を変えずに、最新の機能(新しい家具)を使い続けることができます。便利ですよね。
ここが盲点!:初期化関数の「鍵」問題
この仕組みで最も重要なのが「誰が家具を入れ替えていいのか」という権限です。
もし、家具を入れ替えるための「マスターキー」を作るためのコマンド(initialize関数と言います)に、誰もが自由にアクセスできる状態だったらどうなるでしょう?
そう、攻撃者がやってきて、自分を「新しい大家さん」として登録し、中身を丸ごと自分の作った悪意あるコントラクトにすり替えてしまうのです。これが初期化関数の保護不備による乗っ取りのメカニズムです。
—
2. 実演:やってはいけない「初期化」のコード
まずは、やってはいけない「無防備なコード」を見てみましょう。
// 危険な例:誰でも初期化できてしまう
function initialize(address _owner) public {
// 誰でもこの関数を呼べてしまうため、攻撃者が自分をownerに設定可能!
owner = _owner;
}
これだと、デプロイした瞬間に誰かがこの関数を叩いて、「今のオーナーは私です!」と書き換えてしまいます。家を建てた瞬間に、通りすがりの人に鍵を渡しているようなものです。
—
3. どう防ぐ?:防御の鉄則「Initializer」
では、どうすれば安全なのでしょうか?答えはシンプル、「一度しか開かない金庫」にすることです。
対策のコード例
OpenZeppelinなどのライブラリを活用し、initializer修飾子を使いましょう。
// 安全な例:一度しか実行できないようにする
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract MyContract is Initializable {
address public owner;
function initialize(address _owner) public initializer {
// initializer修飾子により、この関数は一度しか実行できません
owner = _owner;
}
}
さらに、UUPSパターンを使う場合は、コンストラクタで初期化を封じる工夫も重要です。
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
// コンストラクタで初期化をロックし、実行後の攻撃を防ぐ
_disableInitializers();
}
—
4. 現場の知見:なぜ事故は起きるのか?
セキュリティリサーチャーとして現場に立つと、技術的なミス以上に「運用の油断」が原因であるケースをよく見かけます。
- デプロイ後の即時実行を忘れる:
「後で設定しよう」と思っている間に、ボットがネットワーク上を監視していて、デプロイの数秒後に乗っ取り関数を叩いてくることがあります。
- テストコードの甘さ:
「初期化が誰でも実行できるテスト」を書いていない、あるいは「初期化後の所有権確認」をテスト項目から漏らしているケースが多いです。
—
まとめ:今日からできる防犯対策
1. initializerを必ず使う: 関数が複数回呼ばれないよう、ライブラリの制約を徹底しましょう。
2. デプロイと同時に所有権を確定させる: initialize関数を呼び出すまでを一つのトランザクションに含めるか、自動化スクリプトで完璧に管理しましょう。
3. 「自分だけがアクセスできるか」を常に疑う: 自分の書いた関数が、外部から叩かれたらどうなるか?という視点を常に持ってください。
セキュリティは「完璧な防御」を目指すものではなく、「泥棒が諦めるような面倒な仕組みを作る」ことです。
難しく聞こえるかもしれませんが、一歩ずつコードの意味を理解すれば、あなたのコントラクトは格段に硬くなります。これからも一緒に、安全なWeb3ライフを作っていきましょう!
何か不明な点や、「こんなコードはどう?」という質問があれば、いつでもコメントしてくださいね。エンジニア同士、知恵を出し合っていきましょう!
コメント