こんにちは!ITの世界へようこそ。新人エンジニアの皆さん、日々の開発やお仕事お疲れ様です。
新しい技術を学ぶとき、専門用語の壁にぶつかって「ううっ」と頭を抱えてしまうことってありませんか?セキュリティの分野も、なんだか難しそうな言葉ばかりで尻込みしてしまいますよね。でも、大丈夫です!一歩ずつ、身近な例えから紐解いていけば必ず理解できます。
今回は、Web3(ブロックチェーン)の世界でたまに耳にする「ガス制限によるDoS攻撃(Gas Limit DoS)」という少し怖い名前の脆弱性について、一緒に優しく学んでいきましょう!
—
1. 家のポストに例えて理解する「ガス制限」の仕組み
まずは、ブロックチェーンの世界が現実世界のどんな仕組みに似ているか、イメージしてみましょう。
皆さんの家の玄関には、郵便受け(ポスト)がありますよね。
普段なら、ハガキや手紙が数通ポロッと入るだけで、ポストがいっぱいになることはありません。
しかし、もし悪意ある誰かが、「世界中から集めたありとあらゆるチラシを、1回の配達であなたのポストに一気に詰め込もうとしたら」どうなるでしょうか?
- ポストの容量がパンクしてしまいます。
- すべてのチラシを押し込みきれず、配達員さんは作業を諦めて帰ってしまいます。
- その結果、本当に届くべき大切な手紙(家族からの連絡など)まで、ポストに入らなくなってしまいます。
ブロックチェーンにおける「ガス(Gas)」とは、まさにこの「ポストの容量(あるいは作業にかける燃料)」のようなものです。イーサリアムなどのスマートコントラクトでは、トランザクション(処理)ごとに「ここまでしか燃料を使っちゃダメですよ」という上限(ガスリミット)が決められています。
もしプログラムの中で「無限に続くループ処理」などを書いてしまうと、その処理は膨大な燃料を食いつぶし、上限に達して強制的にエラー(失敗)になってしまいます。これが、攻撃者によって引き起こされるガス制限によるDoS攻撃(Gas Limit DoS)の正体です。
—
2. 脆弱なスマートコントラクトの姿を見てみよう
百聞は一見にしかず。実際のコードを見ながら、どこが危ないのかを確認してみましょう。
以下のコードは、参加者全員に一斉にお金を配る(払い戻す)機能を持った、一見すると便利そうなスマートコントラクトです。しかし、ここに大きな落とし穴があります。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// 【危険な例】プッシュ型(一括処理)のコントラクト
badRefundContract {
address[] public investors; // 投資家のリスト
// 投資家を追加する関数
function addInvestor() public payable {
investors.push(msg.sender);
}
// 全員に一斉にお金を返す関数(危険!)
function refundAll() public {
// 投資家の人数が増えれば増えるほど、ループ回数が増加する
for (uint256 i = 0; i < investors.length; i++) {
// 一人ひとりに送金処理を行う(プッシュ型)
(bool success, ) = investors[i].call{value: 1 ether}("");
require(success, "Transfer failed"); // 送金が失敗したら全体がストップ
}
}
}
何が問題なのでしょうか?
このコードのやり方は、先ほどの例で言えば「配達員(スマートコントラクト)が、1軒1軒のお宅を回って、自分で大量の荷物を無理やり押し込もうとしている状態」です。
- 投資家が10人くらいなら問題なく動きます。
- しかし、サービスが人気になって投資家が10,000人に増えたらどうなるでしょう?
refundAll()を実行した瞬間、10,000人分のループ処理が走ります。ブロックチェーンが許容する1回あたりの作業量(ガス制限)をあっさり超えてしまい、トランザクションは必ず失敗(Revert)します。
こうなると、誰もお金を引き出せなくなり、サービスは実質的に機能停止(Denial of Service)に追い込まれます。これが、プッシュ型の限界が生む恐ろしい罠です。
—
3. 対策の切り札:「プッシュ型」から「プル型」への変更
では、どうすればこのピンチを回避できるのでしょうか?
答えは、配達のスタイルをガラリと変えることにあります。
先ほどの「こちらから無理やり配る(プッシュ型)」をやめて、「各自で取りに来てもらう(プル型)」という仕組みに変えればいいのです。現実世界で言えば、各々が郵便局の窓口へ行って、自分の分を受け取ってくるイメージですね。
実際の修正コードを見てみましょう。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// 【安全な例】プル型(各自で引き出す)のコントラクト
contract SafeRefundContract {
// 投資家ごとの残高を管理するマップ
mapping(address => uint256) public balances;
// 投資家が自分で資金を引き出す関数(プル型)
function withdrawRefund() public {
uint256 amount = balances[msg.sender];
// 未払い分があるかチェック
require(amount > 0, "No funds to withdraw");
// 二重取りを防ぐために、先に残高をゼロにする
balances[msg.sender] = 0;
// 本人に送金する
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
// 管理者が残高を登録する(一括ループを排除)
function setBalance(address investor, uint256 amount) public {
balances[investor] = amount;
}
}
プル型に変えるメリット
この安全なコードでは、全員分のループ処理を一切行っていません。
投資家はそれぞれ自分のタイミングで withdrawRefund() を呼び出し、自分の分だけを引き出します。
- リストの人数が何万人になろうとも、1回あたりの処理は「1人分の引き出し」だけで完結します。
- ガス制限を超えてトランザクションが失敗するリスクを綺麗になくすことができますよね。
これこそが、スマートコントラクト開発において非常に重要な「プル型パターンへの移行」という鉄則です。
—
まとめ
今回は、ガス制限によるDoS攻撃の仕組みと、その対策であるプル型パターンについて学びました。
- 攻撃のメカニズム: 人数やデータ量に比例してループが無限に膨らみ、ガスの限界を超えてシステムが止まってしまう。
- 防御の基本方針: 「こちらから押し込む(プッシュ型)」のをやめ、「自分で取りに来てもらう(プル型)」設計に変える。
セキュリティの対策と聞くと身構えてしまいますが、根底にあるのは「無理のない仕組みをつくる」という現実世界の優しさと同じです。
一歩ずつ、こうした実務的なノウハウを身につけて、信頼されるエンジニアを目指していきましょう!次回の解説もお楽しみに!
コメント