この記事の対象読者
- ソフトウェアテストの基礎知識がある方
- セキュリティテストに興味があるが、ファジングを試したことがない方
- LLMやAIがセキュリティ分野でどう活用されているか知りたい方
この記事で得られること
- ファジングの基本概念と3つの主要手法(ミューテーション型/ジェネレーション型/カバレッジガイド型)を理解できる
- Google OSS-Fuzzの実績データと、AIファジングが変えたゲーム: 1,300プロジェクト・13,000脆弱性発見の裏側がわかる
- Pythonで実際にファジングを体験する方法: Atherisを使った実践コードとCI/CDへの組み込み方がわかる
この記事で扱わないこと
- ファジングエンジンの内部実装(libFuzzer/AFL++のソースコード解説)
- カーネルファジング・ハードウェアファジングなどの高度な専門領域
- 脆弱性の悪用手法(攻撃側の具体的テクニック)
1. ファジングとの出会い
「20年間見つからなかった脆弱性を、AIがたった数週間で12個見つけた」
先日の記事で紹介した、OpenSSLの歴史的な事件。この発見の背景にあった技術が**ファジング(Fuzzing)**だ。
正直に言うと、私が初めてファジングという言葉を聞いたとき、「なんかランダムにデータを突っ込むやつでしょ?」程度の認識だった。テストと言えばユニットテストやE2Eテストが頭に浮かぶし、ファジングは「セキュリティの専門家がやるニッチなもの」というイメージがあった。
しかし、OpenSSLの事件を調べるうちに認識が変わった。ファジングは、2026年の今、AIの力で「全開発者が知るべきテスト手法」に変貌しつつある。
料理に例えるなら、ファジングは「レシピ通りに作れるか確認する味見」ではなく、**「わざと変な材料を入れて、キッチンが爆発しないか確認する破壊テスト」**だ。
ここまでで、ファジングがどんな位置づけの技術なのか、イメージが湧いたと思う。次は、この記事で使う用語を整理しておこう。
2. 前提知識の確認
本題に入る前に、この記事で登場する用語を確認する。
2.1 ファジング(Fuzzing)とは
ソフトウェアに対して、意図的に不正・予期しない・ランダムなデータを入力として与え、クラッシュや異常動作を引き起こすバグを発見する自動テスト手法。1988年にウィスコンシン大学のBarton Miller教授が、Unixユーティリティにランダムデータを入力する実験で提唱した。
2.2 ファザー(Fuzzer)とは
ファジングを実行するツールのこと。テスト対象に送る入力データを生成し、プログラムの異常動作を検出する。代表的なファザーにはGoogle開発のlibFuzzer、AFL++(American Fuzzy Lop plus plus)、Honggfuzzなどがある。
2.3 コードカバレッジとは
テスト実行時に、プログラムのソースコードのうちどの部分が実行されたかを示す指標。カバレッジが高いほど、多くのコードパスがテストされていることを意味する。ただし、カバレッジ100%でもバグがないとは限らない——これがファジングの重要な教訓だ。
2.4 サニタイザー(Sanitizer)とは
プログラム実行中のメモリエラーや未定義動作を検出するツール。AddressSanitizer(ASan)、MemorySanitizer(MSan)、UndefinedBehaviorSanitizer(UBSan)などがある。ファジングとサニタイザーを組み合わせることで、クラッシュしないが危険な動作(メモリリーク、バッファオーバーリードなど)も発見できる。
これらの用語が押さえられたら、ファジングが生まれた背景を見ていこう。
3. ファジングが生まれた背景
3.1 1988年:偶然から生まれたテスト手法
ファジングの起源は、1988年のウィスコンシン大学に遡る。Barton Miller教授が嵐の夜にリモート接続でUnixシステムを操作していたところ、回線のノイズでランダムな文字がコマンドに混入し、多くのUnixユーティリティがクラッシュした。この「偶然の発見」から、意図的にランダム入力でプログラムを壊すテスト手法が生まれた。
Miller教授のチームが最初の論文で報告した結果は衝撃的だった——テストしたUnixユーティリティの25〜33%がランダム入力でクラッシュした。
3.2 2014年以降:Heartbleedが変えた世界
2014年、OpenSSLのHeartbleed脆弱性(CVE-2014-0160)が発見され、世界中のHTTPS通信が危機にさらされた。この事件をきっかけに、オープンソースソフトウェアのセキュリティ監査が大幅に強化された。
Googleは2016年にOSS-Fuzzプロジェクトを立ち上げ、オープンソースプロジェクトに対する無料の継続的ファジングサービスを開始した。
| 年 | マイルストーン |
|---|---|
| 1988年 | Barton Miller教授がファジングを提唱 |
| 2006年 | Google、Chromeのセキュリティにファジングを本格導入 |
| 2014年 | Heartbleed発見、OSS監査の強化 |
| 2016年 | Google OSS-Fuzz開始 |
| 2023年 | Google、OSS-FuzzにAI(LLM)を統合 |
| 2024年 | AI生成ファズターゲットで26件の脆弱性発見(OpenSSL含む) |
| 2026年 | AISLEのAIがOpenSSLで12件のゼロデイを発見 |
3.3 なぜ今ファジングが再注目されているのか
従来のファジングは「ランダムにデータを投げる力技」であり、膨大な計算資源と専門知識が必要だった。しかしAI/LLMの進化により、状況が根本的に変わりつつある。
| 従来のファジング | AIファジング |
|---|---|
| ランダムな入力生成 | コード構造を理解した入力生成 |
| 人間がファズターゲットを手動作成 | LLMがファズターゲットを自動生成 |
| クラッシュの分析は人間が行う | AIがクラッシュを自動分類・トリアージ |
| 専門家のみが運用可能 | CI/CDに統合して全チームが利用可能 |
背景がわかったところで、基本的な仕組みを見ていこう。
4. 基本概念と仕組み
4.1 ファジングの3つの主要手法
ファジングは入力データの生成方法によって大きく3つに分類される。
ミューテーション型(Mutation-based)
既存の有効な入力データ(シード)を元に、ビット反転、バイト追加、値の変更などの変異(ミューテーション)を加えて新しい入力を生成する。最も一般的なアプローチ。
# ミューテーション型の概念イメージ
import random
def mutate(seed: bytes) -> bytes:
"""シードデータにランダムな変異を加える"""
data = bytearray(seed)
mutation_type = random.choice(["flip", "insert", "delete", "replace"])
if mutation_type == "flip" and len(data) > 0:
# ランダムなビットを反転
pos = random.randint(0, len(data) - 1)
data[pos] ^= (1 << random.randint(0, 7))
elif mutation_type == "insert":
# ランダムなバイトを挿入
pos = random.randint(0, len(data))
data.insert(pos, random.randint(0, 255))
elif mutation_type == "delete" and len(data) > 1:
# ランダムなバイトを削除
pos = random.randint(0, len(data) - 1)
del data[pos]
elif mutation_type == "replace" and len(data) > 0:
# ランダムなバイトを置換
pos = random.randint(0, len(data) - 1)
data[pos] = random.randint(0, 255)
return bytes(data)
# 使用例
seed = b'{"name": "test", "value": 42}'
for i in range(5):
mutated = mutate(seed)
print(f"Mutation {i+1}: {mutated}")
ジェネレーション型(Generation-based)
入力データのフォーマット仕様(文法)を定義し、仕様に基づいて新しい入力をゼロから生成する。プロトコルファジングやファイルフォーマットのテストに有効。
カバレッジガイド型(Coverage-guided)
現在の主流。プログラムのコードカバレッジをフィードバックとして利用し、未到達のコードパスを探索するよう入力生成を最適化する。libFuzzerやAFL++がこの方式を採用している。
4.2 ファジングの動作フロー
┌─────────┐ ┌──────────┐ ┌──────────────┐ ┌─────────────┐
│ シード │ → │ 変異 │ → │ テスト対象に │ → │ 結果を監視 │
│ コーパス │ │ エンジン │ │ 入力を投入 │ │ (クラッシュ?)│
└─────────┘ └──────────┘ └──────────────┘ └──────┬──────┘
↑ │
│ カバレッジフィードバック │
└────────────────────────────────────────────────────┘
4.3 AIが変えたファジングのパラダイム
2023年8月、GoogleはOSS-FuzzにLLMを統合した。AIは主に以下の領域でファジングを強化している。
| AI活用領域 | 内容 | 効果 |
|---|---|---|
| ファズターゲット自動生成 | LLMがコードを分析し、テスト対象の関数を特定・ファズターゲットを自動生成 | 人間の作業を大幅に削減 |
| 入力文法の推論 | AIが入力サンプルからフォーマット構造を学習 | 複雑なプロトコルのファジングが容易に |
| クラッシュトリアージ | AIがクラッシュを自動分類し、重複排除・重大度判定 | セキュリティチームの負担を削減 |
| 適応的変異戦略 | 強化学習で変異オペレータの選択を最適化 | カバレッジの効率的な拡大 |
Google OSS-Fuzzは2025年5月時点で、1,300以上のプロジェクトで13,000以上の脆弱性と50,000以上のバグを発見している。
基本概念が理解できたところで、実際にコードを書いて動かしてみよう。
5. 実践:Pythonでファジングを体験する
5.1 環境構築
Pythonのファジングには、Googleが開発したAtheris(Pythonネイティブのカバレッジガイドファザー)を使用する。
# 必要なパッケージのインストール
pip install atheris hypothesis
5.2 環境別の設定ファイル
開発環境用(fuzzing_config.yaml)
# fuzzing_config.yaml - 開発環境用(このままコピーして使える)
environment: development
fuzzer:
engine: atheris
timeout_per_input: 5 # 秒
max_total_time: 300 # 5分(開発中は短めに)
corpus_dir: ./corpus/dev
artifact_dir: ./crashes/dev
sanitizer:
address: true
undefined: true
reporting:
level: DEBUG
output: stdout
本番CI用(fuzzing_config.production.yaml)
# fuzzing_config.production.yaml - CI/CDパイプライン用
environment: production
fuzzer:
engine: atheris
timeout_per_input: 30
max_total_time: 3600 # 1時間(CI/CDで十分な時間を確保)
corpus_dir: ./corpus/ci
artifact_dir: ./crashes/ci
jobs: 4 # 並列実行数
sanitizer:
address: true
undefined: true
memory: true
reporting:
level: WARNING
output:
- file: ./fuzzing_report.json
- slack_webhook: ${SLACK_SECURITY_WEBHOOK}
gate:
fail_on_crash: true
block_merge: true
テスト環境用(fuzzing_config.test.yaml)
# fuzzing_config.test.yaml - テスト/検証環境用
environment: test
fuzzer:
engine: atheris
timeout_per_input: 10
max_total_time: 600 # 10分
corpus_dir: ./corpus/test
artifact_dir: ./crashes/test
sanitizer:
address: true
undefined: false # テスト環境では速度優先
reporting:
level: INFO
output: ./fuzzing_test_report.json
5.3 基本的なファジング:JSON パーサーをテスト
"""
json_fuzzer.py - PythonのJSONパーサーに対するファジング
実行方法: python json_fuzzer.py
"""
import atheris
import sys
import json
def test_json_parser(data: bytes):
"""JSONパーサーに対するファズターゲット"""
try:
decoded = data.decode("utf-8", errors="ignore")
# パース試行
parsed = json.loads(decoded)
# パース成功した場合、再シリアライズしてラウンドトリップチェック
re_serialized = json.dumps(parsed)
re_parsed = json.loads(re_serialized)
except (json.JSONDecodeError, UnicodeDecodeError, ValueError):
# これらの例外は「期待される失敗」なので無視
pass
except Exception as e:
# 予期しない例外はバグの可能性
raise
def main():
"""メイン処理"""
atheris.Setup(
sys.argv,
test_json_parser,
enable_python_coverage=True # Pythonコードのカバレッジ計測を有効化
)
atheris.Fuzz()
if __name__ == "__main__":
main()
5.4 実践的なファジング:自作バリデーターのバグを見つける
ここからが本番だ。意図的にバグを含む関数を書いて、ファジングで発見してみよう。
"""
validator_fuzzer.py - 自作バリデーターのバグをファジングで発見する
実行方法: python validator_fuzzer.py
"""
import atheris
import sys
def validate_user_input(data: str) -> dict:
"""
ユーザー入力をバリデーションする関数(意図的にバグを含む)
フォーマット: "name:age:email"
"""
parts = data.split(":")
if len(parts) != 3:
raise ValueError("Invalid format: expected name:age:email")
name, age_str, email = parts
# バグ1: 名前の長さチェックが甘い(空文字を許容してしまう)
if len(name) > 100:
raise ValueError("Name too long")
# バグ2: age のパースで整数オーバーフローの可能性
age = int(age_str) # 負の値や巨大な値を検証していない
# バグ3: emailの@チェックが不完全
if "@" not in email:
raise ValueError("Invalid email")
# バグ4: 特定の文字列でクラッシュ(シミュレーション)
if name == "CRASH":
result = [1, 2, 3]
return result[999] # IndexError!
return {"name": name, "age": age, "email": email}
def test_validator(data: bytes):
"""バリデーターに対するファズターゲット"""
try:
input_str = data.decode("ascii", errors="ignore")
result = validate_user_input(input_str)
except (ValueError, UnicodeDecodeError):
# 期待される検証エラーは無視
pass
except Exception as e:
# ValueError以外の例外はバグ!
print(f"[BUG FOUND] Input: {data!r}")
print(f"[BUG FOUND] Exception: {type(e).__name__}: {e}")
raise
def main():
atheris.Setup(sys.argv, test_validator, enable_python_coverage=True)
atheris.Fuzz()
if __name__ == "__main__":
main()
5.5 実行結果
上記のコードを実行すると、以下のような出力が得られる:
$ python validator_fuzzer.py -max_total_time=60
INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 2847391056
INFO: -max_total_time is set to 60
...
INFO: New: 1 L: 15 MS: 1 CMP- DE: "CRASH"-
[BUG FOUND] Input: b'CRASH:25:a@b.com'
[BUG FOUND] Exception: IndexError: list index out of range
==12345== ERROR: libFuzzer: deadly signal
...
artifact_prefix='./'; Test unit written to ./crash-abc123def456
ファジングは数秒〜数分で「CRASH」という入力値を発見し、IndexErrorが発生することを報告してくれる。人間がテストケースを書いていたら見落としていたかもしれないバグだ。
5.6 よくあるエラーと対処法
| エラー | 原因 | 対処法 |
|---|---|---|
ModuleNotFoundError: No module named 'atheris' |
Atheris未インストール |
pip install atheris を実行 |
RuntimeError: Coverage not available |
Cython版Atherisがビルドできていない |
pip install atheris --no-binary atheris でソースビルド |
BARF: No valid inputs in corpus |
コーパスディレクトリが空 | 初回はコーパスなしで実行可能。シードファイルを追加するとより効率的 |
timeout per unit exceeded |
1入力あたりのタイムアウト超過 | ファズターゲットの処理を軽量化するか、-timeout 値を増やす |
Windows環境で atheris がインストールできない |
AtherisはLinux/macOS向け | WSL2を使用するか、代替としてhypothesisを使用 |
5.7 環境診断スクリプト
#!/usr/bin/env python3
"""
ファジング環境診断スクリプト
実行方法: python check_fuzz_env.py
"""
import sys
import shutil
import subprocess
def check_environment():
"""ファジング環境をチェックして問題を報告"""
issues = []
# Python バージョン確認
if sys.version_info < (3, 9):
issues.append(f"Python 3.9以上が必要です(現在: {sys.version})")
else:
print(f" [OK] Python {sys.version}")
# Atheris確認
try:
import atheris
print(f" [OK] atheris がインストール済み")
except ImportError:
issues.append("atheris がインストールされていません: pip install atheris")
# Hypothesis確認(プロパティベーステスト用)
try:
import hypothesis
print(f" [OK] hypothesis {hypothesis.__version__}")
except ImportError:
issues.append("hypothesis 未インストール(推奨): pip install hypothesis")
# Clang/LLVM確認(C/C++ファジング用)
if shutil.which("clang"):
result = subprocess.run(["clang", "--version"], capture_output=True, text=True)
version_line = result.stdout.split('\n')[0]
print(f" [OK] {version_line}")
else:
issues.append("clang が見つかりません(C/C++ファジングに必要)")
# AFL++確認
if shutil.which("afl-fuzz"):
print(" [OK] AFL++ がインストール済み")
else:
issues.append("AFL++ 未インストール(推奨): apt install afl++")
# サマリー
print()
if issues:
print(" 問題が見つかりました:")
for issue in issues:
print(f" - {issue}")
else:
print(" 環境は正常です。ファジングを始められます!")
if __name__ == "__main__":
print("=" * 50)
print(" ファジング環境診断ツール v1.0")
print("=" * 50)
check_environment()
5.8 GitHub Actionsでの自動ファジング(CI/CD統合)
# .github/workflows/fuzz.yml
name: Fuzzing CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
fuzz:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install atheris
pip install -r requirements.txt
- name: Run fuzzing (5 minutes)
run: |
python validator_fuzzer.py -max_total_time=300 -artifact_prefix=./crashes/
- name: Upload crash artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: fuzzing-crashes
path: ./crashes/
実装方法がわかったので、次は具体的なユースケースを見ていく。
6. ユースケース別ガイド
6.1 ユースケース1: Webアプリケーションの入力バリデーションテスト
想定読者: PythonやJavaScriptでWebアプリを開発している方
推奨構成: Hypothesis(プロパティベーステスト)+ pytest で、ユニットテストにファジング的アプローチを導入
サンプルコード:
"""
web_input_fuzz.py - Hypothesisを使ったWebアプリ入力のファジング
実行方法: pytest web_input_fuzz.py -v
"""
from hypothesis import given, strategies as st, settings
import re
def sanitize_username(username: str) -> str:
"""ユーザー名をサニタイズする関数(テスト対象)"""
if not username or len(username) > 50:
raise ValueError("Invalid username length")
# 英数字とアンダースコアのみ許可
sanitized = re.sub(r'[^a-zA-Z0-9_]', '', username)
if len(sanitized) == 0:
raise ValueError("Username contains no valid characters")
return sanitized.lower()
@given(st.text(min_size=0, max_size=200))
@settings(max_examples=10000)
def test_sanitize_username_never_crashes(username):
"""サニタイズ関数がどんな入力でもクラッシュしないことを検証"""
try:
result = sanitize_username(username)
# サニタイズ結果の事後条件チェック
assert isinstance(result, str)
assert len(result) <= 50
assert result == result.lower()
assert re.match(r'^[a-z0-9_]+$', result)
except ValueError:
pass # ValueErrorは期待される失敗
@given(st.text(alphabet=st.characters(whitelist_categories=('L', 'N')),
min_size=1, max_size=50))
def test_sanitize_valid_input_preserves_content(username):
"""有効な入力が正しくサニタイズされることを検証"""
result = sanitize_username(username)
assert len(result) > 0
6.2 ユースケース2: APIのエンドポイントファジング
想定読者: REST APIを開発・運用している方
推奨構成: Schemathesis(OpenAPI仕様ベースのAPIファジング)
サンプルコード:
# Schemathesisのインストール
pip install schemathesis
# OpenAPI仕様からAPIを自動ファジング
schemathesis run http://localhost:8000/openapi.json \
--checks all \
--stateful=links \
--max-response-time=5000
"""
api_fuzz.py - Schemathesisを使ったAPI自動ファジング(Pythonスクリプト版)
"""
import schemathesis
schema = schemathesis.from_url("http://localhost:8000/openapi.json")
@schema.parametrize()
def test_api(case):
"""OpenAPI仕様に基づいてAPIを自動ファジング"""
response = case.call()
case.validate_response(response)
# サーバーエラー(5xx)は許容しない
assert response.status_code < 500
6.3 ユースケース3: データパイプラインの堅牢性テスト
想定読者: データエンジニアリングやML基盤を構築している方
推奨構成: Hypothesisの@givenデコレータでデータ変換関数をテスト
サンプルコード:
"""
data_pipeline_fuzz.py - データ変換関数のファジング
実行方法: pytest data_pipeline_fuzz.py -v
"""
from hypothesis import given, strategies as st
import math
def normalize_score(value: float, min_val: float, max_val: float) -> float:
"""スコアを0〜1の範囲に正規化する関数(テスト対象)"""
if min_val >= max_val:
raise ValueError("min must be less than max")
if math.isnan(value) or math.isinf(value):
raise ValueError("value must be finite")
normalized = (value - min_val) / (max_val - min_val)
return max(0.0, min(1.0, normalized))
@given(
value=st.floats(allow_nan=True, allow_infinity=True),
min_val=st.floats(min_value=-1e6, max_value=1e6),
max_val=st.floats(min_value=-1e6, max_value=1e6),
)
def test_normalize_score_handles_all_floats(value, min_val, max_val):
"""正規化関数がどんなfloat値でも安全に処理できることを検証"""
try:
result = normalize_score(value, min_val, max_val)
assert 0.0 <= result <= 1.0, f"Result {result} out of range"
assert not math.isnan(result), "Result is NaN"
except ValueError:
pass # 期待される検証エラー
ユースケースを把握できたところで、この先の学習パスを確認しよう。
7. 学習ロードマップ
この記事を読んだ後、次のステップとして以下をおすすめする。
初級者向け(まずはここから)
- Hypothesis公式ドキュメントでプロパティベーステストの基本を学ぶ
- Google OSS-Fuzzの公式リポジトリでプロジェクト一覧と実績を確認する
- 自分のプロジェクトの入力バリデーション関数にHypothesisを適用してみる
中級者向け(実践に進む)
- Atherisで自分のPythonプロジェクトのファズターゲットを作成する
- SchemathesisでREST APIのファジングを導入する
- GitHub ActionsやGitLab CIにファジングジョブを組み込む
上級者向け(さらに深く)
- AFL++でC/C++プロジェクトのファジングに挑戦する
- Google FuzzBenchでファジングエンジンの性能比較を学ぶ
- ClusterFuzzLiteで自分のプロジェクトにOSS-Fuzz相当の環境を構築する
8. まとめ
この記事では、ファジングについて以下を解説した。
- ファジングの基本概念と歴史: 1988年の偶然の発見から、2026年のAI統合まで
- 3つの主要手法: ミューテーション型、ジェネレーション型、カバレッジガイド型の違い
- Pythonでの実践方法: Atheris/Hypothesisを使った具体的なコードとCI/CD統合
私の所感
OpenSSLの12件のゼロデイ発見を調べる中で、ファジングの進化に驚かされた。特にGoogleのOSS-Fuzzが「OpenSSLに対して139個のファザーを並行運用している」というデータは衝撃的だった。
しかし同時に、GitHub Security Labの研究者が指摘するように、OSS-Fuzzに登録されていても、カバレッジが低いプロジェクトでは脆弱性が何年も見逃されている。GStreamerのカバレッジはわずか19%で、7年間ファジングされていたにもかかわらず29件の新規脆弱性が発見された。
ファジングは「導入して終わり」ではない。ファズターゲットの継続的な改善、カバレッジの監視、新しい攻撃面の追加——この地味な運用こそが、本当のセキュリティを支えている。
そしてAIは、この「地味な運用」を自動化する力を持っている。私の見解では、2026年以降、AIファジングはユニットテストと同じくらい「当たり前のテスト手法」になると考えている。
参考文献
- Google OSS-Fuzz - GitHub
- Google Security Blog - Leveling Up Fuzzing: Finding more vulnerabilities with AI (2024)
- GitHub Blog - Bugs that survive the heat of continuous fuzzing (2025)
- Atheris - Google's Python Fuzzing Engine
- Hypothesis - Property-Based Testing for Python
- Thoughtworks - Fuzz-testing in the AI era (2025)
この記事が役に立ったら、いいね・ストックしていただけると励みになります。
関連記事: