Slitherで「脆弱性の芽」をCI/CDで摘む:スマートコントラクト防衛の最前線
現場でスマートコントラクトを扱う際、「コードを書いてから監査に出す」というフローだけでは、もはや時代遅れだ。デプロイ直前の監査は最終防衛ラインであって、日常的な開発の中では、脆弱性は「書いた瞬間に検知」しなければならない。
特に、Web3のインフラを支えるスマートコントラクトにおいて、再入攻撃(Reentrancy)のような古典的かつ致命的なバグを放置することは、銀行の金庫を全開にして放置するのと同じだ。今日は、Trail of Bits社が開発した静的解析ツール「Slither」をCI/CDに組み込み、開発者の指先から脆弱性を排除する泥臭い実務テクニックを伝授する。
—
1. なぜSlitherなのか?:泥臭い現場の直感
多くのエンジニアは、動的解析(ファジング)や専門家による手動監査を重視する。確かに重要だが、それらは高コストで時間がかかる。Slitherは、コンパイル後のAST(抽象構文木)を解析することで、コードの論理構造を高速にスキャンする。
例えば、reentrancy-eth(ETH送信後の外部コール)のような、人間がレビューで「見落としがちな」ミスを、Slitherは数秒で見抜く。CI/CDに組み込めば、脆弱なコードをプルリクエスト(PR)時点でブロックできる。これが「堅牢な開発」の第一歩だ。
—
2. CI/CDへの実装:GitHub Actionsで自動防衛線を張る
GitHub Actionsを利用して、コードがプッシュされるたびにSlitherを走らせるワークフローを作成しよう。これにより、脆弱性のあるコードがメインブランチにマージされることを物理的に防ぐ。
以下の .github/workflows/slither.yml をプロジェクトのルートに配置してくれ。
name: Slither Analysis
on: [push, pull_request]
jobs:
analyze:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Slither
run: pip3 install slither-analyzer
- name: Run Slither
# --detectで特定の脆弱性カテゴリに絞ることも可能だが、
# 最初は全チェックで「何が起きているか」を把握しよう
# CIを落とすために --fail-pedantic を付与
run: slither . --detect reentrancy-eth,uninitialized-state,shadowing-state --fail-pedantic
この設定の肝は --fail-pedantic だ。これをつけることで、Slitherが警告を1つでも検知すれば、CIが失敗し、マージがブロックされる。開発者には「苦い薬」だが、これこそがチームを守る盾になる。
—
3. 実践:再入攻撃の「死の罠」とセキュアな実装
攻撃者が狙うのは、call を使って外部コントラクトを呼び出す際、状態変数が更新される前に行われる隙だ。以下の脆弱なコードを見てほしい。
脆弱な実装(NGパターン)
// 警告:Slitherはこの行の後の外部コールを検知して再入攻撃を指摘する
function withdraw(uint256 _amount) public {
require(balances[msg.sender] >= _amount);
(bool success, ) = msg.sender.call{value: _amount}(""); // ここで制御権が攻撃者に渡る
require(success);
balances[msg.sender] -= _amount; // 更新が遅すぎる!
}
このコードに対し、Slitherは「Reentrancy in withdraw」と警告を出す。これを防ぐための標準的かつ強力なアプローチは「Checks-Effects-Interactionsパターン」を徹底することだ。
セキュアな実装(OKパターン)
// セキュアな実装:先に状態を更新し、最後に外部コールを行う
function withdraw(uint256 _amount) public {
// 1. Checks: バリデーション
require(balances[msg.sender] >= _amount, "Insufficient balance");
// 2. Effects: 状態変数の更新(先にやる!)
balances[msg.sender] -= _amount;
// 3. Interactions: 外部コール
(bool success, ) = msg.sender.call{value: _amount}("");
require(success, "Transfer failed");
}
このように、状態変数の更新を外部コールより「先」に行うだけで、再入攻撃の脆弱性は根絶される。SlitherをCIに組み込んでおけば、うっかりこの順序を逆に書いた瞬間にビルドが失敗するため、インシデントは発生の余地すらなくなる。
—
4. セキュリティチーフからの提言
Slitherは万能ではない。複雑なビジネスロジックの脆弱性や、経済的設計の欠陥(Flash Loan攻撃など)までは見抜けないこともある。しかし、「型のない言語で、型チェックなしにコードを書く」ような無謀な真似をAIとツールに監視させることは、今の時代、プロフェッショナルとして最低限の礼儀だ。
もし君が現場で、「Slitherがうるさくて開発効率が落ちる」という声を聞いたら、こう返してやってくれ。
> 「バグを修正するコストと、本番環境で資金が流出した時の対応コストを天秤にかけてみろ。その『うるさいツール』こそが、君のキャリアを守っているんだ。」
次に導入すべきは、slither-check-ks を使ったカスタムルールや、複雑なプロジェクトに向けた Slitherin の導入だ。これについては、また別の機会に深く語るとしよう。まずは、今のCIにSlitherをねじ込むところから始めてくれ。
コメント