2025年12月18日木曜日

Python で fuzzing テスト

Python で fuzzing テスト

概要

メモリのオーバフローテストなどできます

環境

  • Ubuntu 24.04
  • Python 3.12.10
  • atheris 3.0.0

テスト対象のコード

  • vim fuzzing.py
import ipaddress


def get_ipaddress(data: dict) -> str | None:
    """
    入力のdictから 'ipaddress' キーの値を取得して返却する。
    
    Args:
        data: 入力辞書
        
    Returns:
        str | None: 有効なIPアドレス、またはキーが存在しない場合はNone
        
    Raises:
        ValueError: IPアドレスのフォーマットが不正な場合
    """
    if "ipaddress" not in data:
        return None
    
    ip_str = data["ipaddress"]
    
    # IPアドレスのフォーマットをチェック
    try:
        # IPv4 または IPv6 アドレスとして検証
        ipaddress.ip_address(ip_str)
        return ip_str
    except ValueError as e:
        raise ValueError(f"Invalid IP address format: {ip_str}") from e

テストコード

  • vim test_fuzzing.py
import sys
import atheris
from fuzzing import get_ipaddress


@atheris.instrument_func
def test_get_ipaddress(data):
    """
    get_ipaddress関数のファジングテスト
    """
    if len(data) < 1:
        return
    
    # ランダムなバイトデータを文字列に変換
    try:
        input_str = data.decode('utf-8', errors='ignore')
    except Exception:
        return
    
    # テストケース1: キーが存在しない場合
    try:
        result = get_ipaddress({})
        assert result is None
    except Exception:
        pass
    
    # テストケース2: ipaddressキーに様々な値を設定
    try:
        result = get_ipaddress({"ipaddress": input_str})
        # 結果が返ってきた場合は文字列であることを確認
        if result is not None:
            assert isinstance(result, str)
    except ValueError:
        # フォーマットエラーは想定内
        pass
    except Exception as e:
        # その他の予期しない例外は記録
        print(f"Unexpected exception: {type(e).__name__}: {e}")
        raise
    
    # テストケース3: 他のキーも含む辞書
    try:
        test_dict = {
            "other_key": "value",
            "ipaddress": input_str,
            "another_key": 123
        }
        result = get_ipaddress(test_dict)
        if result is not None:
            assert isinstance(result, str)
    except ValueError:
        pass
    except Exception as e:
        print(f"Unexpected exception in test case 3: {type(e).__name__}: {e}")
        raise


def main():
    """
    ファジングテストのエントリーポイント
    """
    atheris.Setup(sys.argv, test_get_ipaddress)
    atheris.Fuzz()


if __name__ == "__main__":
    main()

おまけ: クラスの場合

import ipaddress


class IPAddressValidator:
    """IPアドレスの検証を行うクラス"""

    def get_ipaddress(self, data: dict) -> str | None:
        """
        入力のdictから 'ipaddress' キーの値を取得して返却する。

        Args:
            data: 入力辞書

        Returns:
            str | None: 有効なIPアドレス、またはキーが存在しない場合はNone

        Raises:
            ValueError: IPアドレスのフォーマットが不正な場合
        """
        if "ipaddress" not in data:
            return None

        ip_str = data["ipaddress"]

        # IPアドレスのフォーマットをチェック
        try:
            # IPv4 または IPv6 アドレスとして検証
            ipaddress.ip_address(ip_str)
            return ip_str
        except ValueError as e:
            raise ValueError(f"Invalid IP address format: {ip_str}") from e
import sys

import atheris
from fuzzing import IPAddressValidator


class FuzzingTest:
    """IPAddressValidatorのファジングテストクラス"""

    def __init__(self):
        self.validator = IPAddressValidator()

    @atheris.instrument_func
    def test_get_ipaddress(self, data):
        """
        get_ipaddressメソッドのファジングテスト
        """
        if len(data) < 1:
            return

        # ランダムなバイトデータを文字列に変換
        try:
            input_str = data.decode("utf-8", errors="ignore")
        except Exception:
            return

        # テストケース1: キーが存在しない場合
        try:
            result = self.validator.get_ipaddress({})
            assert result is None
        except Exception:
            pass

        # テストケース2: ipaddressキーに様々な値を設定
        try:
            result = self.validator.get_ipaddress({"ipaddress": input_str})
            # 結果が返ってきた場合は文字列であることを確認
            if result is not None:
                assert isinstance(result, str)
        except ValueError:
            # フォーマットエラーは想定内
            pass
        except Exception as e:
            # その他の予期しない例外は記録
            print(f"Unexpected exception: {type(e).__name__}: {e}")
            raise

        # テストケース3: 他のキーも含む辞書
        try:
            test_dict = {
                "other_key": "value",
                "ipaddress": input_str,
                "another_key": 123,
            }
            result = self.validator.get_ipaddress(test_dict)
            if result is not None:
                assert isinstance(result, str)
        except ValueError:
            pass
        except Exception as e:
            print(f"Unexpected exception in test case 3: {type(e).__name__}: {e}")
            raise


def main():
    """
    ファジングテストのエントリーポイント
    """
    fuzzing_test = FuzzingTest()
    atheris.Setup(sys.argv, fuzzing_test.test_get_ipaddress)
    atheris.Fuzz()


if __name__ == "__main__":
    main()

おまけ: pytest で書く

なさそうです

最後に

テスト中は CPU 100% になるので注意しましょう
かなり負荷がかかるテストをするのでデータベースや外部にリクエストする関数をテストする場合は必ずモックするようにしましょう

またテストの量も膨大で上記のコードでも30分くらいかかりました
速く終わらせたい場合はスペックのあるマシン上でテストしてください

参考サイト

0 件のコメント:

コメントを投稿