「誰でも管理者になれる」という悪夢 —— スマートコントラクトのアクセス制御不備を撲滅する
現場で数々のプロジェクトを見てきたが、いまだに「なぜここを公開してしまったのか?」と絶句するようなコントラクトに出くわすことがある。特に、SCADAのような重要インフラとWeb3のブリッジを構築する際、この「アクセス制御の不備(Unprotected Function Call)」は致命的な死因となる。
「開発中だから」「とりあえず動くようにした」という甘えが、数億円単位のハッキング被害を招く。今回は、なぜこれが防げないのか、そしてどうすれば「絶対に破られない」権限管理を実装できるのか、泥臭い知見を共有する。
—
1. なぜ「公開関数」は狙われるのか?
Web3の世界では、コントラクト内の関数はデフォルトで public であることが多い。もしあなたが、資産を移動させる withdraw() や、管理者権限を付与する setAdmin() といったクリティカルな関数に、何のガードもかけていなければ、それは玄関の鍵を開けっ放しにして「泥棒に入らないでください」と看板を出しているのと同じだ。
攻撃者は、EtherscanやBscScan、あるいは自前のスクリプトでコントラクトのABI(Application Binary Interface)を解析している。彼らにとって、public でガードのない関数は「宝箱を開けるためのボタン」でしかない。
攻撃のPoC(概念実証)
攻撃者は以下のような単純なスクリプトを走らせ、数ミリ秒で全資金を吸い取る。
// 攻撃者の視点:ガードのないコントラクトを叩くだけのスクリプト
const contractAddress = "0x..."; // 脆弱なコントラクトのアドレス
const abi = ["function withdrawAll()"]; // ABIから関数名を特定
const contract = new ethers.Contract(contractAddress, abi, attackerWallet);
// 誰でも呼び出せるため、署名さえあれば即座に資金が引き出される
await contract.withdrawAll();
console.log("資金を全額回収しました。");
これだけで、あなたのプロトコルは終わる。
—
2. 修飾子(Modifier)による鉄壁の防御
OpenZeppelinの Ownable や AccessControl は、単なるライブラリではない。これは「先人の屍の上に築かれた防壁」だ。これを適切に使うだけで、脆弱性の9割は防げる。
セキュアな実装例(Solidity)
以下のコードは、onlyOwner 修飾子を用いて、コントラクトの作成者以外の呼び出しを物理的にブロックする実装だ。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/access/Ownable.sol";
contract SecureVault is Ownable {
// コンストラクタでオーナーを設定
constructor() Ownable(msg.sender) {}
// 重要:onlyOwner 修飾子を必ず付与する
// これにより、msg.sender == owner であることが保証される
function withdrawFunds() external onlyOwner {
payable(owner()).transfer(address(this).balance);
}
// 設定変更も同様に権限を絞る
function updateConfig(uint256 _newRate) external onlyOwner {
// 設定ロジック
}
}
ポイント:
externalを使用することで、コントラクト内からの呼び出しを制限し、ガス代を節約しつつ外部からの呼び出しに特化させる。onlyOwner修飾子一つで、未承認のトランザクションは全てrevert(ロールバック)される。
—
3. インフラ・バックエンド側の二重防御(多層防御)
スマートコントラクト単体に頼るな。IoTデバイスやバックエンドと連携する場合、APIゲートウェイやWAFでのアクセス制御も組み合わせて「多層防御」を敷くのがプロの仕事だ。
もし、コントラクトを操作するWeb APIを構築しているなら、Nginxレベルでリクエストをフィルタリングし、不正なIPや過剰なアクセスを遮断しておくべきだ。
Nginx設定例(アクセス制御のヒント)
# 特定の管理者IP以外からのアクセスを拒否する設定例
location /api/admin/ {
allow 192.168.1.10; # 管理者の固定IPのみ許可
deny all; # それ以外は403 Forbiddenを返す
proxy_pass http://backend_cluster;
}
—
4. 現場のシニアエンジニアからの教訓
最後に、これだけは覚えておいてほしい。
1. 「テストネットで動いた」は安心材料にならない: メインネットにデプロイする前に、必ず MythX や Slither といった静的解析ツールを回せ。これらは人間が見落とす public 関数の露出を自動で指摘してくれる。
2. 権限付与は「最小権限の原則」を徹底せよ: 誰でも管理者になれる Ownable は論外だが、逆に「オーナー権限」が強すぎると、オーナーの秘密鍵が漏れた瞬間にシステムが死ぬ。AccessControl を使い、役割(Role)ごとに権限を細分化(RBAC)するのが、現代のスタンダードだ。
3. インシデントハンドリングの準備: もし万が一、権限を奪われたと検知したら、即座に「緊急停止スイッチ(Circuit Breaker)」を発動できるよう、コントラクトに pause() 機能を組み込んでおくこと。
技術は日々進化するが、「アクセス制御」という基本だけは変わらない。君たちのコードが、誰かの資産やインフラを支えているという誇りを持って、今日からこのガードを実装してほしい。
何かあればいつでもコードを持ってこい。一緒にレビューしよう。
コメント