こんにちは!スマートコントラクトの開発の世界へようこそ。
ブロックチェーンやWeb3の世界って、なんだか最先端で難しそうに見えますよね。「自分の書いたコードが世界中に公開されて、一瞬でお金が動くなんて緊張する…」と感じている新人開発者の方も多いのではないでしょうか。
今回は、そんなスマートコントラクトにおける「アクセス制御の不備と権限昇格攻撃」について、身近な「家の鍵」の防犯にたとえながら、優しく一歩ずつ紐解いていきたいと思います。
セキュリティの泥臭い現実を知ることは、頑丈で安心できる「デジタルのお家」を建てるための第一歩です。一緒に楽しく学んでいきましょう!
—
1. ブロックチェーンの「家の鍵」はどうなっている?
突然ですが、みなさんのお家には玄関の鍵がありますよね。お出かけするときには鍵を閉めますし、合鍵を渡す相手も信用できる家族や親しい友人に限るはずです。もし、近所の誰もが自由に開け閉めできる「合鍵」が玄関に刺さったままだったら……想像するだけでゾッとしますよね。
スマートコントラクトの世界もこれとまったく同じです。
ブロックチェーン上にデプロイ(公開)されたプログラム(コントラクト)は、一度動き出すと誰にも止められません。もし、特定の重要な操作(お金を引き出す、設定を変える、コントラクトを停止するなど)を行うための「鍵」が誰でも使える状態になっていたら、悪意を持った攻撃者にすべてを奪われてしまいます。
これが、今回お話しする「アクセス制御の不備」というセキュリティの落とし穴なんです。
—
2. よくある落とし穴:誰もがオーナーになれる「初期化関数」の罠
では、実際のスマートコントラクトで、どのようなミスが起きやすいのか見ていきましょう。
最近のスマートコントラクト(特にアップグレード可能なプロキシパターンなど)では、コントラクトをデプロイしたあとに初期設定を行うため、initialize() のような「初期化関数」を用意することがよくあります。
ここで、新人開発者さんがやってしまいがちな「うっかりミス」の例を見てみましょう。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract VulnerableVault {
address public owner;
// 【危険な実装】誰でも何回でも呼べてしまう初期化関数
function initialize() public {
// オーナーが誰に設定されているかチェックしていない!
owner = msg.sender; // 最後にこの関数を呼んだ人がオーナーになってしまう
}
// お金を引き出す大事な関数
function withdraw() public {
// オーナーしか引き出せないはず…
require(msg.sender == owner, "Owner only!");
// 引き出し処理(省略)
}
}
このコード、どこが危ないか分かりますか?
答えは、initialize() 関数に「すでに初期化されていたら実行をストップする」というガードマン(チェック機構)がいないことです。
泥棒の手口:鍵が刺さるのを待ち構える
現実の世界でたとえるなら、ハウスメーカーが新しい家を建てた後、玄関の鍵を閉め忘れて「最初に来た人にこの家の権利をあげます」という看板を立ててしまっているような状態です。
攻撃者は、ブロックチェーンのネットワークを常に監視しています。新しいコントラクトがデプロイされた瞬間を見つけ出し、誰よりも早くこの initialize() 関数を呼び出します。すると、コントラクトの owner には攻撃者のアドレスが設定されてしまい、一瞬でコントラクトが乗っ取られてしまうのです。これが権限昇格攻撃のメカニズムです。
—
3. 一歩ずつ学ぶ!安全な「鍵」のかけ方
「じゃあ、どうやって守ればいいの?」という話ですよね。安心してください。OpenZeppelinなどの信頼できるライブラリが用意してくれている標準的な仕組みを使えば、このリスクは簡単に防ぐことができます。
先ほどのような初期化関数を安全にするには、Initializable というモジュールを継承させ、initializer 修飾子(モディファイア)をくっつけます。
安全なコードの書き方を見てみましょう。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// OpenZeppelinの安全な初期化管理ライブラリをインポート
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
contract SecureVault is Initializable, OwnableUpgradeable {
// コンストラクタの代わりに使う初期化関数
function initialize() public initializer {
// 親コントラクトの初期化を呼びつつ、デプロイした人をオーナーに設定
__Ownable_init();
}
// お金を引き出す大事な関数
function withdraw() public onlyOwner {
// Ownableライブラリが提供する onlyOwner 修飾子がガードマンの役割をしてくれます
// 引き出し処理(省略)
}
}
ここがポイント!
1. initializer 修飾子の使用:
initialize() 関数に initializer をつけることで、この関数は「人生で一度しか実行できない」ようにロックされます。2回目以降に誰かが実行しようとしても、プログラムが自動でエラーにして弾いてくれます。
2. onlyOwner 修飾子の活用:
「オーナーだけが実行できる」という機能を自分でゼロから書く必要はありません。Ownable などの実績あるライブラリを使うことで、安全な鍵管理を簡単に実装できます。
—
4. 実務で活かすチェックリストと開発者の心得
スマートコントラクトの開発現場では、コードを書くスピードと同じくらい、あるいはそれ以上に「レビュー」のプロセスが大切になります。
明日からの開発で、ぜひ次のポイントをチェックしてみてください。
- [ ] アクセス制御の関数(
transferOwnership,initialize,setAdminなど)には、必ず適切な制限(onlyOwnerやinitializer)がついているか? - [ ] コントラクトのデプロイ後、すぐに初期化関数を呼び出すトランザクションを安全に実行しているか?(デプロイと初期化が別のトランザクションになる場合、その間の隙を突かれない手順になっているか?)
- [ ] 自分以外の誰かが勝手に重要な設定を変えられないテスト(単体テスト)を書いているか?
セキュリティに初めて触れるときは、覚えることが多くて大変に感じるかもしれません。でも、「誰がこの操作をしていいんだっけ?」と立ち止まって考える習慣こそが、あなたを優秀なWeb3エンジニアにしてくれる最高の武器になります。
一歩ずつ、安全で信頼されるスマートコントラクトを作っていきましょう!
コメント