【実務・中級編】 スマートコントラクトにおける整数オーバーフロー/アンダーフローの現代的対策 – IoT・OT(制御システム) & ブロックチェーンセキュリティ防御ガイド

整数オーバーフローの「正体」:Solidity 0.8.x 以降でも油断できない理由

現場でコードレビューをしていると、未だに「Solidity 0.8.0 以上を使っているからSafeMathは不要だよね?」という甘い認識に出くわすことがある。確かにコンパイラレベルで組み込みチェックが導入されたのは革命的だった。しかし、脆弱性は常に「想定外の場所」に潜んでいる。

今回は、かつてDeFiプロジェクトを壊滅させた整数オーバーフロー/アンダーフローの恐怖と、現代における「本当の対策」を、泥臭い実務の視点から解説する。

—

1. そもそも「何が起きるのか」:PoCの恐怖

オーバーフローとは、変数のデータ型が保持できる最大値を超えた際に、値がゼロに戻ってしまう現象だ(アンダーフローはその逆)。

例えば、uint8(0~255)の変数が 255 の状態で + 1 されると、値は 0 になる。攻撃者はこの挙動を悪用し、トークンの残高を無限に増やしたり、計算ロジックをバイパスして資金を抜き取る。

攻撃イメージ(概念的な疑似コード)

もし古いコントラクトで、残高チェックを回避するコードがあったらどうなるか:

// 脆弱な残高更新ロジックの例
function withdraw(uint _amount) public {
    // 残高チェック:balance[msg.sender] >= _amount
    // ここで uint256 のアンダーフローを誘発させる
    require(balance[msg.sender] - _amount >= 0); 
    
    balance[msg.sender] -= _amount;
    msg.sender.transfer(_amount);
}

攻撃者が _amount に balance[msg.sender] + 1 を渡すと、balance[msg.sender] - _amount の計算結果が uint256 の最大値(アンダーフロー)になり、require 文を突破してしまう。これが、かつて数多のプロトコルが潰れた「算術脆弱性」の正体だ。

—

2. 現代の鉄則:Solidity 0.8.x 以降の正しい付き合い方

Solidity 0.8.0 からは、デフォルトで算術演算がオーバーフロー/アンダーフローを検知してリバート(Revert)するようになった。つまり、明示的に SafeMath をインポートして add() や sub() を呼ぶ必要はなくなった。

しかし、ここで注意が必要だ。

「意図的な」アンダーフローが必要な場合

ガス代削減のために、あえてアンダーフローを許容したい特殊なケース(例:ビット演算や特定の暗号技術)がある場合、unchecked ブロックを使う必要がある。

// Solidity 0.8.x 以降の正しい「あえて計算する」実装
function increment(uint256 x) public pure returns (uint256) {
    unchecked {
        // ここではオーバーフローチェックが無効になる
        // 意図的に計算を高速化したい場合のみ使用すること
        return x + 1;
    }
}

この unchecked ブロックは、「なぜここでチェックを外したのか」をコードコメントで明記しない限り、チームのレビューを通過させてはならない。これが現代のセキュリティ・ガバナンスだ。

—

3. 実践:Web側(Python/JavaScript)でのフロントガード

スマートコントラクト側で防ぐのは大前提だが、クライアント側(Web3フロントエンド)でも、ユーザーが不正な数値を入力できないようにバリデーションをかけるのが「多層防御」の基本だ。

JavaScript (ethers.js) での入力バリデーション例

ユーザーが送信する前に、BN(BigNumber)を使用してクライアント側で計算を行い、オーバーフローの兆候がないか確認する。

import { ethers } from "ethers";

/**
 * 送信前のバリデーション関数
 */
function validateTransferAmount(balance, amount) {
    const bal = ethers.BigNumber.from(balance);
    const amt = ethers.BigNumber.from(amount);

    // 送金額が残高を超えていないか事前にチェック
    if (amt.gt(bal)) {
        throw new Error("残高不足です。攻撃的なリクエストをブロックしました。");
    }
    
    // 0以下の入力も弾く
    if (amt.lte(0)) {
        throw new Error("不正な金額です。");
    }
    
    return true;
}

—

4. インフラ・運用レベルで防ぐ「盲点」

セキュリティリサーチャーとして最後に言いたいのは、コードだけでなく「運用」にも目を向けろということだ。

  • コンパイラ設定の固定: hardhat.config.js や foundry.toml で、Solidityのバージョンが 0.8.0 未満にダウングレードされないように厳格に固定すること。
  • WAFによるペイロード検査: Nginx や CloudFront の AWS WAF を活用し、明らかに異常なサイズの整数値(例えば、Web3のトランザクションデータに含まれる過大な数値)をフィルタリングするルールを設定しておくことも、DDoS対策として有効だ。

推奨する Nginx 設定の考え方(一部抜粋)

APIゲートウェイ等でWeb3ライブラリを介して値を渡す際、不自然に長い16進数文字列を制限する。

# nginx.conf: 不当な長いリクエストボディの制限
# 異常な整数値を大量に送る攻撃を未然に防ぐ
client_body_buffer_size 16k;
client_max_body_size 16k;

—

まとめ:結局のところ「人間」が最大の脆弱性

整数オーバーフローは、もはや「防ぐのが難しい脆弱性」ではない。コンパイラが自動でやってくれる今の時代、我々エンジニアが気をつけるべきは「ツールを過信せず、ロジックの妥当性を常に疑うこと」だ。

unchecked を使うときは、その行に「なぜこれが必要なのか」を熱く語るコメントを残せ。そして、フロントエンドのバリデーションをサボるな。

セキュリティとは、ツールを導入して終わりではない。コードの向こう側にいる攻撃者の顔を想像し、彼らが通るであろう道を先回りして塞ぎ続ける、地道なプロフェッショナリズムの積み重ねなんだ。明日からの開発で、ぜひ意識してほしい。

コメント

タイトルとURLをコピーしました