mcpField
P
performing-api-fuzzing-with-restler

performing-api-fuzzing-with-restler

github.com/mukul975セキュリティ35.2k180 回インストール8 週間前に更新
リポジトリ

performing-api-fuzzing-with-restler について

API セキュリティテスト担当者向けの RESTler ファジングガイドです。OpenAPI 仕様からテスト文法を生成し、ステートフルなリクエスト列の検査とバグ分類を扱います。

主な機能

  1. 01OpenAPI からファジング文法を生成
  2. 02ステートフルな API 呼び出し列を検査
  3. 03認証とテスト設定の構成
  4. 04簡易または完全なファジングを実行
  5. 05発見したバグを種類別に整理

活用シーン

  1. 01ステージング環境で REST API を検査
  2. 02リリース前に API の変更を確認
  3. 03特定の呼び出し順で起きる不具合を発見
  4. 04CI/CD でセキュリティ回帰テストを実行

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/mukul975/anthropic-cybersecurity-skills --skill performing-api-fuzzing-with-restler

インストール後、Claude Code で/performing-api-fuzzing-with-restlerを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

LICENSE
references/
api-reference.md
scripts/
agent.py

詳細

RESTler を使用した API ファジング

使用する場面

  • OpenAPI/Swagger 仕様を使用して、REST API の自動セキュリティテストを実施する場合
  • 特定の API 呼び出しシーケンスでのみ発生するバグを検出する場合(ステートフルテスト)
  • 未処理の例外やクラッシュ状態を示す 500 Internal Server Error の応答を見つける場合
  • 不正な形式、境界値、インジェクションのペイロードでパラメーターをファジングし、API の入力検証をテストする場合
  • CI/CD パイプラインで API の変更に対する継続的なセキュリティ回帰テストを実施する場合

明示的な許可と監視なしに、本番環境に対して使用しないでください。RESTler はファジング中にリソースを積極的に作成・削除します。

前提条件

  • 対象 API と許容されるテスト範囲を明記した書面による許可
  • Python 3.12+ と .NET 8.0 ランタイムがインストールされていること
  • https://github.com/microsoft/restler-fuzzer から RESTler をダウンロードしていること
  • 対象 API の OpenAPI/Swagger 仕様(v2 または v3)
  • API 認証情報(トークン、API キー、または OAuth 認証情報)
  • 隔離されたテスト/ステージング環境(RESTler は 1 時間あたり数千のリソースを作成する可能性があります)

ワークフロー

ステップ 1:RESTler のインストールとセットアップ
# Clone and build RESTler
git clone https://github.com/microsoft/restler-fuzzer.git
cd restler-fuzzer

# Build RESTler
python3 ./build-restler.py --dest_dir /opt/restler

# Verify installation
/opt/restler/restler/Restler --help

# Alternative: Use pre-built release
# Download from https://github.com/microsoft/restler-fuzzer/releases
ステップ 2:API 仕様をコンパイルする
# Compile the OpenAPI spec into a RESTler fuzzing grammar
/opt/restler/restler/Restler compile \
    --api_spec /path/to/openapi.yaml

# Output directory structure:
# Compile/
#   grammar.py          - Generated fuzzing grammar
#   grammar.json        - Grammar in JSON format
#   dict.json           - Custom dictionary for fuzzing values
#   engine_settings.json - Engine configuration
#   config.json         - Compilation config

対象を絞ったファジング用のカスタム辞書(dict.json):

{
    "restler_fuzzable_string": [
        "fuzzstring",
        "' OR '1'='1",
        "\" OR \"1\"=\"1",
        "<script>alert(1)</script>",
        "../../../etc/passwd",
        "${7*7}",
        "{{7*7}}",
        "a]UNION SELECT 1,2,3--",
        "\"; cat /etc/passwd; echo \"",
        "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
    ],
    "restler_fuzzable_int": [
        "0",
        "-1",
        "999999999",
        "2147483647",
        "-2147483648"
    ],
    "restler_fuzzable_bool": ["true", "false", "null", "1", "0"],
    "restler_fuzzable_datetime": [
        "2024-01-01T00:00:00Z",
        "0000-00-00T00:00:00Z",
        "9999-12-31T23:59:59Z",
        "invalid-date"
    ],
    "restler_fuzzable_uuid4": [
        "00000000-0000-0000-0000-000000000000",
        "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
    ],
    "restler_custom_payload": {
        "/users/{userId}": ["1", "0", "-1", "admin", "' OR 1=1--"],
        "/orders/{orderId}": ["1", "0", "999999999"]
    }
}
ステップ 3:認証を設定する
# authentication_token.py - RESTler authentication module
import requests
import json
import time

class AuthenticationProvider:
    def __init__(self):
        self.token = None
        self.token_expiry = 0
        self.auth_url = "https://target-api.example.com/api/v1/auth/login"
        self.credentials = {
            "email": "[email protected]",
            "password": "FuzzerPass123!"
        }

    def get_token(self):
        """Get or refresh authentication token."""
        current_time = time.time()
        if self.token and current_time < self.token_expiry - 60:
            return self.token

        resp = requests.post(self.auth_url, json=self.credentials)
        if resp.status_code == 200:
            data = resp.json()
            self.token = data["access_token"]
            self.token_expiry = current_time + 3600  # Assume 1-hour TTL
            return self.token
        else:
            raise Exception(f"Authentication failed: {resp.status_code}")

    def get_auth_header(self):
        """Return the authentication header for RESTler."""
        token = self.get_token()
        return f"Authorization: Bearer {token}"

# Export the token refresh command for RESTler
auth = AuthenticationProvider()
print(auth.get_auth_header())

認証用のエンジン設定(engine_settings.json):

{
    "authentication": {
        "token": {
            "token_refresh_interval": 300,
            "token_refresh_cmd": "python3 /path/to/authentication_token.py"
        }
    },
    "max_combinations": 20,
    "max_request_execution_time": 30,
    "global_producer_timing_delay": 2,
    "no_ssl": false,
    "host": "target-api.example.com",
    "target_port": 443,
    "garbage_collection_interval": 300,
    "max_sequence_length": 10
}
ステップ 4:テストモード(スモークテスト)で RESTler を実行する
# Test mode: Quick validation that all endpoints are reachable
/opt/restler/restler/Restler test \
    --grammar_file Compile/grammar.py \
    --dictionary_file Compile/dict.json \
    --settings Compile/engine_settings.json \
    --no_ssl \
    --target_ip target-api.example.com \
    --target_port 443

# Review test results
cat Test/ResponseBuckets/runSummary.json
# Parse test results
import json

with open("Test/ResponseBuckets/runSummary.json") as f:
    summary = json.load(f)

print("Test Mode Summary:")
print(f"  Total requests: {summary.get('total_requests_sent', {}).get('num_requests', 0)}")
print(f"  Successful (2xx): {summary.get('num_fully_valid', 0)}")
print(f"  Client errors (4xx): {summary.get('num_invalid', 0)}")
print(f"  Server errors (5xx): {summary.get('num_server_error', 0)}")

# Identify uncovered endpoints
covered = summary.get('covered_endpoints', [])
total = summary.get('total_endpoints', [])
uncovered = set(total) - set(covered)
if uncovered:
    print(f"\nUncovered endpoints ({len(uncovered)}):")
    for ep in uncovered:
        print(f"  - {ep}")
ステップ 5:Fuzz-Lean モードを実行する
# Fuzz-lean: One pass through all endpoints with security checkers enabled
/opt/restler/restler/Restler fuzz-lean \
    --grammar_file Compile/grammar.py \
    --dictionary_file Compile/dict.json \
    --settings Compile/engine_settings.json \
    --target_ip target-api.example.com \
    --target_port 443 \
    --time_budget 1  # 1 hour max

# Checkers automatically enabled in fuzz-lean:
# - UseAfterFree: Tests accessing resources after deletion
# - NamespaceRule: Tests accessing resources across namespaces/tenants
# - ResourceHierarchy: Tests child resources with wrong parent IDs
# - LeakageRule: Tests for information disclosure in error responses
# - InvalidDynamicObject: Tests with malformed dynamic object IDs
ステップ 6:フルファジングモードを実行する
# Full fuzz mode: Extended fuzzing for comprehensive coverage
/opt/restler/restler/Restler fuzz \
    --grammar_file Compile/grammar.py \
    --dictionary_file Compile/dict.json \
    --settings Compile/engine_settings.json \
    --target_ip target-api.example.com \
    --target_port 443 \
    --time_budget 4 \
    --enable_checkers UseAfterFree NamespaceRule ResourceHierarchy LeakageRule InvalidDynamicObject PayloadBody

# Analyze fuzzing results
python3 <<'EOF'
import json
import os

results_dir = "Fuzz/ResponseBuckets"
bugs_dir = "Fuzz/bug_buckets"

# Parse bug buckets
if os.path.exists(bugs_dir):
    for bug_file in os.listdir(bugs_dir):
        if bug_file.endswith(".txt"):
            with open(os.path.join(bugs_dir, bug_file)) as f:
                content = f.read()
            print(f"\n=== Bug: {bug_file} ===")
            print(content[:500])

# Parse response summary
summary_file = os.path.join(results_dir, "runSummary.json")
if os.path.exists(summary_file):
    with open(summary_file) as f:
        summary = json.load(f)
    print(f"\nFuzz Summary:")
    print(f"  Duration: {summary.get('time_budget_hours', 0)} hours")
    print(f"  Total requests: {summary.get('total_requests_sent', {}).get('num_requests', 0)}")
    print(f"  Bugs found: {summary.get('num_bugs', 0)}")
    print(f"  500 errors: {summary.get('num_server_error', 0)}")
EOF

主な概念

用語定義
ステートフルファジング以前のリクエストへの応答を後続のリクエストの入力として使用し、リクエスト間の状態を維持する API ファジングです。複数ステップのワークフローをテストできます
プロデューサー/コンシューマーの依存関係ある API 呼び出しで生成された値(作成されたリソース ID など)を後続の呼び出しで使用すべきだとする、RESTler による推論です
ファジング文法各エンドポイントで有効なリクエストと無効なリクエストを生成する方法を定義した、API 仕様のコンパイル済み表現です
チェッカーuse-after-free、名前空間の分離、情報漏えいなど、特定の脆弱性パターンをテストする RESTler のセキュリティルールです
バグバケット効率的なトリアージのため、発見されたバグを種類とエンドポイント別に分類し、類似する失敗をグループ化する RESTler の機能です
ガベージコレクション対象システムでのリソース枯渇を防ぐため、ファジング中に作成されたリソースを RESTler が定期的にクリーンアップすることです

ツールとシステム

  • RESTler:OpenAPI 仕様をファジング文法にコンパイルする、Microsoft Research のステートフル REST API ファジングツールです
  • Schemathesis:OpenAPI/GraphQL スキーマからテストケースを生成する、プロパティベースの API テストツールです
  • Dredd:API の実装を OpenAPI/API Blueprint のドキュメントに照らして検証する API テストツールです
  • Fuzz-lightyear:認証および認可の脆弱性の発見に重点を置いた、Yelp のステートレス API ファザーです
  • API Fuzzer:カスタマイズ可能なペイロード辞書を使用して API エンドポイントをファジングする OWASP のツールです

よくあるシナリオ

シナリオ:マイクロサービス API のファジングキャンペーン

コンテキスト:あるフィンテック企業は、OpenAPI 仕様を備えた 12 個のマイクロサービス API を運用しています。大規模リリースの前に、セキュリティチームはステージング環境で各サービスに対して RESTler のファジングを実行し、バグを検出します。

アプローチ:

  1. 12 個すべてのサービスの OpenAPI 仕様を収集し、それぞれを RESTler の文法にコンパイルします
  2. サービスごとの認証情報を使用して、各サービスの認証を設定します
  3. 各サービスでテストモードを実行し、エンドポイントに到達できることを検証して、文法の問題を修正します
  4. ファズリーンモードを実行します(サービスごとに 1 時間)。すぐに対処できる問題を特定します
  5. ファズリーンモードで 23 件のバグを発見します。内訳は、処理されない 500 エラーが 8 件、解放後使用のパターンが 5 件、名前空間の分離に関する不具合が 4 件、エラーレスポンスでの情報漏えいが 6 件です
  6. バグが最も多かった 5 つのサービスで、フルファズモードを実行します(サービスごとに 4 時間)
  7. ユーザーを削除してトークンを再利用してもアクセスが許可される、重大な認証バイパスを含む追加の 47 件のバグを発見します
  8. バグレポートを生成し、JIRA 連携を通じて修正状況を追跡します

落とし穴:

  • RESTler が何千ものリソースを作成・削除することを理解せず、本番環境に対して実行する
  • 認証を正しく設定せず、RESTler が未認証アクセスだけをテストする状態になる
  • デフォルトの辞書を使用し、アプリケーション固有のインジェクション用ペイロードを追加しない
  • 時間制限を設定せず、RESTler を無期限に実行させる
  • 依存関係の問題によって RESTler が到達できないエンドポイントを示すコンパイル時の警告を無視する

出力形式

## RESTler API Fuzzing Report

**Target**: User Service API (staging.example.com)
**Specification**: OpenAPI 3.0 (42 endpoints)
**Duration**: 4 hours (full fuzz mode)
**Total Requests**: 145,832

### Bug Summary

| Category | Count | Severity |
|----------|-------|----------|
| 500 Internal Server Error | 12 | High |
| Use After Free | 3 | Critical |
| Namespace Rule Violation | 5 | Critical |
| Information Leakage | 8 | Medium |
| Resource Leak | 4 | Low |

### Critical Findings

**1. Use-After-Free: Deleted user token still valid**
- Sequence: POST /users -> DELETE /users/{id} -> GET /users/{id}
- After deleting user, GET with the deleted user's token returns 200
- Impact: Deleted accounts can still access the API

**2. Namespace Violation: Cross-tenant data access**
- Sequence: POST /users (tenant A) -> GET /users/{id} (tenant B token)
- User created by tenant A is accessible with tenant B's credentials
- Impact: Multi-tenant isolation breach

**3. 500 Error: Unhandled integer overflow**
- Request: POST /orders {"quantity": 2147483648}
- Response: 500 Internal Server Error with stack trace
- Impact: DoS potential, information disclosure via stack trace

### Coverage

- Endpoints covered: 38/42 (90.5%)
- Uncovered: POST /admin/migrate, DELETE /admin/cache,
  PUT /config/advanced, POST /webhooks/test

コメント