TL;DR: あなたのAPIの入力は攻撃対象領域なので、そのようにテストしてください。サイズが大きすぎるフィールド、間違った型、不正な形式のボディ、インジェクション文字列を送信するネガティブケースを作成し、エンドポイントが4xxで応答し、決して5xxで応答しないことを確認してください。`additionalProperties: false`、enum、および長さ制限を使用して、スキーマ検証をセキュリティ制御に変えましょう。すべての変更に対してCIでスイート全体を実行します。AIエージェントはこれを緊急の課題にしています。彼らはペイロードを機械の速度で生成・転送するため、「このデータをロードする」が「このコードを実行する」と静かに変化することが、今や大規模に発生しています。
ほとんどのテストスイートは、呼び出し元が丁寧な場合にAPIが機能することを証明します。有効なボディを送信すると200が返され、アサーションが成功します。この結果は、ボディが敵対的である場合に何が起こるかについてはほとんど何も教えてくれません。信頼できない入力とは、エンドポイント自身が生成しなかったあらゆるデータです。リクエストボディ、クエリ文字列、ヘッダー、ファイルアップロード、Webhookペイロード、そしてAIエージェントがその場で組み立てるJSONなどです。これらすべてに対し、誰かが最終的に最悪のバージョンを送信するだろうという同じ仮定を置くべきです。
2026年7月、Hugging Faceは、エントリベクターが盗まれたパスワードではなくデータであったセキュリティインシデントについて説明しました。その侵害からの教訓については別途説明しましたが、このガイドは実践的な後半部分です。攻撃者が送信するような種類の入力を送信するテストを構築し、すべての変更で自動的に実行します。カテゴリはOWASP APIセキュリティトップ10と一致しており、これはタブで開いておく価値があります。Apidogは契約を設計し、これらのテストを駆動する一つの方法ですが、このアイデアはすでに使用しているあらゆるフレームワークで通用します。
入力は攻撃対象領域であり、フォームフィールドではない
バリデーションはしばしばユーザーエクスペリエンス上の丁寧な措置として扱われます。空のメールアドレスを検出し、赤い枠を表示して次に進む、といった具合です。この捉え方が問題なのです。あなたのAPIが受け入れるすべてのフィールドは、呼び出し元が破ることができる約束であり、破られた約束の一つ一つがあなたのロジックへの経路となります。小さな整数であると期待していたlimitパラメータが999999999になります。単一の単語であると期待していたfilenameが../../etc/passwdになります。設定を保持すると期待していたconfigオブジェクトが、一連の命令に変わってしまいます。
セキュリティテストは、最後に付け加えられる独立した分野ではありません。それはあなたがすでに知っているのと同じネガティブテストであり、あなたに最も損害を与える可能性のあるフィールドを対象としています。「このフィールドに収まる最悪のものは何か」と問う習慣を身につければ、当社のAPIセキュリティベストプラクティスガイドに示されているプラクティスのほとんどを実践していることになります。この記事の残りの部分では、その一つの質問を実行可能な具体的なテストに変えます。
「このデータをロードする」が「このコードを実行する」になった経緯
Hugging Faceのインシデントは、なぜ入力がこのような注意に値するのかを示す明確な例です。Hugging Faceは、侵入経路が悪意のあるデータセットであったと述べています。細工されたデータセットがリモートコードデータセットローダーをトリガーし、テンプレートインジェクションがデータセット設定内に存在していました。同社の報告は、そのセキュリティインシデントレポートで読むことができます。
その失敗の形をよく考えてみてください。あるエンドポイントはデータとして記述されたものを受け入れました。そのデータをロードすることで、攻撃者が制御する命令を実行できるコードパスが実行されました。「このデータをロードする」が「このコードを実行する」になったのです。テンプレートインジェクションも、より小さな規模で同じ話です。不活性なテキストであるはずだった設定値が評価され、テキストが実行に変わったのです。
教訓は「Hugging Faceが珍しい間違いを犯した」ということではありません。意図したかどうかにかかわらず、ローダー名、フォーマット、テンプレート、シリアル化されたオブジェクト、または設定ブロブを受け入れるエンドポイントは、命令を受け入れているということです。そのエンドポイントに敵対的な設定を送信するテストを一度も書いたことがなければ、それが不活性のままであるという仮定を実際には確認していません。そのテストされていない仮定こそが脆弱性全体なのです。
セキュリティ制御としてのスキーマ検証
追加できる最も安価な制御は、エッジでの厳格なスキーマです。スキーマは単なるドキュメントではありません。一致しないものをすべて拒否するとき、スキーマはビジネスロジックがリクエストを見る前に実行されるフィルターとなります。JSON Schemaは、そのフィルターを厳密にするためのプリミティブを提供します。
以下は、ストーリー中のデータセット設定に対するスキーマで、ほとんどの敵対的な入力がアプリケーションコードに到達しないように書かれています。
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"additionalProperties": false,
"required": ["loader", "name"],
"properties": {
"loader": { "enum": ["csv", "json", "parquet"] },
"name": { "type": "string", "maxLength": 128, "pattern": "^[\\w .-]+$" },
"rows": { "type": "integer", "minimum": 0, "maximum": 1000000 }
}
}
これを4つの別々の防御として読んでください。additionalProperties: falseは密かに挿入されたtemplateフィールドを完全に拒否するため、攻撃者はそれを追加できません。loaderのenumは、pickle://やその他のリモートコードローダーが単に有効な値ではないことを意味します。maxLengthは、メモリを枯渇させることを目的とした数メガバイトの文字列を排除します。nameのpatternは、{{や'; DROP TABLEといった文字がこれ以上進むのを拒否します。これらの行のどれも、攻撃者のことを知っているわけではありません。これらはあなたが実際にサポートする狭い範囲の入力のみを受け入れ、その狭さがセキュリティ特性となるのです。
このような契約検証は、すべてのエクスプロイトを捕捉するわけではなく、いかなるスキーマもそうではありません。これが防ぐのは、特定の一般的なカテゴリ、つまり「このエンドポイントが何を受け入れるか一度もチェックしなかった」というバグです。このカテゴリから、驚くほど多くの侵害が始まります。
ネガティブテスト:エンドポイントが拒否することの証明
ハッピーパスのテストは、良い入力が良い出力を生み出すことを確認します。ネガティブテストは、悪い入力が制御された拒否を生み出すことを確認します。この区別は重要です。なぜなら、拒否は機能だからです。明確なエラーメッセージを伴う400番台の応答は、APIがその境界を防御していることを示します。500番台の応答は、APIがその制御を失っていることを意味します。
ネガティブケースは毎回同じ方法で構築してください。各フィールドについて、拒否すべき内容を書き出します。間違った型、必須なのに欠落している場合、禁止されているのに存在する場合、長すぎる、範囲外、そしてその形式に合うインジェクション文字列などです。次に、応答に対して2つのことを確認します。まず、ステータスが4xxであること(通常は400または422)。次に、ステータスが5xxではないこと。500番台の応答は、あなたの敵対的な入力が、それに対応できていないコードに到達したことを意味し、これはまさに攻撃者が望む到達可能性です。当社のAPIセキュリティテストチェックリストには、適応可能なフィールドごとの開始リストがあります。
この正確さを保つ一つのルールは、エラーテキストではなく、動作についてアサーションを行うことです。「invalid loader」というメッセージが表示されることをアサートした場合、無害なリファクタリングがテストを壊し、チームにそれを緩めるよう教えてしまいます。ステータスコードをアサートし、可能な場合は、副作用が一切発生しなかったことをアサートしてください。
専用テストに値するインジェクションクラス
いくつかのインジェクションファミリーは十分に頻繁に現れるため、一度きりの手動チェックではなく、それぞれに固定のテストケースを設けるべきです。ここでは網羅的である必要はありません。リグレッションが大声で失敗するように、クラスごとに1つのプロービングケースが必要です。自動化されたAPI脆弱性検出を実行するツールは後でカバレッジを広げることができますが、手書きの少数のケースでまず明白な穴を捕捉します。
- SQLインジェクション: クエリに到達する可能性のある任意のフィールドに
1); DROP TABLE datasets;--を送信します。エンドポイントはこれをリテラル値として扱い、400で応答するか、空の結果を返し、データベースエラーを決して表面化させてはなりません。 - テンプレートインジェクション: 名とラベルのフィールドに
{{ 7*7 }}と{{ config.__class__ }}を送信します。もし応答に49が含まれていた場合、テンプレートエンジンがあなたの入力を評価したことになり、それはリモートコード実行が起こりうる状態であることを意味します。 - 安全でない逆シリアル化とリモートコードローダー: 平文の値があるべき場所に、
pickle://のloaderまたはシリアル化されたオブジェクトを送信します。これはまさにHugging Faceの事例と同じ形です。エンドポイントは、不明なローダーを許可リストに基づいて拒否すべきであり、余計な助けをしようとしてはなりません。 - コマンドインジェクション: ファイル名や変換オプションなど、シェル引数になる可能性のある任意のフィールドに
; idや$(id)を送信します。ユーザーIDを漏洩させる200番台の応答は、好奇の対象ではなく、重大な発見です。
サイズ過大、不正な形式、コンテンツタイプ混同
すべての敵対的な入力が巧妙な文字列であるとは限りません。単にサイズが大きすぎたり、形が間違っていたりするものもあり、これらは多くの場合、検証ロジックが実行される前にパーサーを破壊します。
サイズが大きすぎるペイロードを送信します。例えば、1文字の5メガバイトの文字列を保持する単一フィールドや、100万個の要素を持つJSON配列などです。健全なAPIは、メモリを割り当てすぎてダウンするのではなく、ボディサイズの制限を強制し、413を返します。不正な形式のボディも送信してください。切り詰められたJSON、末尾のコンマ、またはスタック枯渇を調査するために千レベル深くネストされたJSONなどです。正しい応答は高速な400であり、ハングしたワーカーではありません。
コンテンツタイプ混同は、静かな問題です。Content-Type: application/jsonと宣言しておきながらXMLを送信したり、application/xmlと宣言して外部エンティティを含むペイロードを送信してXXEを調査したりします。逆に、JSONをtext/plainとして送信し、緩いパーサーがそれでも受け入れるかどうかを確認します。それぞれの不一致は、サーバーがヘッダーを信頼しているのか、ボディを信頼しているのか、または両者が一致することを確認しているのかをテストします。何もパースする前に、一致を要求すべきです。
なぜAIエージェントがリスクを高めるのか
上記のすべては、エージェントが存在する前から当てはまっていました。エージェントは量と速度を変えます。人間の攻撃者は一度に1つの敵対的なリクエストを入力します。AIエージェントは機械の速度でペイロードを生成し転送し、人間が決して試みようとしないような入力を喜んで構築します。
3つの特性がこれを悪化させます。エージェントは入力を合成するため、人間が書いたことのない、テストでも予測できなかったフィールド値を生成します。エージェントは再試行や連鎖呼び出しを行うため、1つの汚染されたアップストリームドキュメントが数秒でエンドポイントに対する数千の敵対的なリクエストに変わる可能性があります。そして、エージェントは信頼するよう指示されたデータを転送するため、データセットやウェブフックに隠されたペイロードが、あなたのAPIへの実際のリクエストになるのです。「このデータをロードする」が「このコードを実行する」に変わるHugging Faceのパターンは、エージェントが信頼境界を越えて、気づくことなく実行してしまうまさにその種の命令です。APIチーム向けのプロンプトインジェクションに関する当社のノートでは、その引き継ぎについてさらに深く掘り下げています。防御策は変わりませんが、エージェントのトラフィックを手動でレビューすることはできないため、自動化されている必要があります。
ネガティブテストスイートを構築し、変更のたびにCIで実行する
上記のケースを、すべてのプルリクエストで実行されるスイートに変換してください。以下は、ステージングエンドポイントを叩き、制御された拒否をアサートするpytestのコンパクトなパラメーター化されたバージョンです。
import httpx
import pytest
BASE = "https://staging.internal/v1"
HOSTILE_CONFIGS = [
{"loader": "pickle://s3/models/payload.pkl", "format": "auto"}, # remote-code loader
{"loader": "csv", "name": "{{ 7*7 }}"}, # template injection
{"loader": "csv", "name": "{{ config.__class__ }}"}, # object traversal
{"loader": "csv", "filter": "1); DROP TABLE datasets;--"}, # SQL injection
{"loader": "csv", "name": "A" * 5_000_000}, # oversized field
]
@pytest.mark.parametrize("config", HOSTILE_CONFIGS)
def test_dataset_config_is_refused(config):
r = httpx.post(f"{BASE}/datasets", json={"config": config}, timeout=10)
assert r.status_code in (400, 413, 422), r.text # a boundary that says no
assert r.status_code < 500, "5xx means the payload reached logic it should not"
assert "49" not in r.text, "template rendered: server-side template injection"
これをCIに組み込んで、マージをゲートする(阻止する)ようにします。最小限のGitHub Actionsジョブでそれが可能です。
name: api-abuse-tests
on: [push, pull_request]
jobs:
negative-input:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pytest tests/negative_input.py -q
ここでスキーマファーストツールがその価値を発揮します。Apidogでは、OpenAPIコントラクトに対してエンドポイントを設計するため、テスト中にすべてのリクエストとレスポンスがそのコントラクトに対してチェックされます。オーバーサイズフィールド、間違った型、上記のインジェクション文字列など、それぞれのステータスが4xxであることをアサートするネガティブシナリオを、ハッピーパスのシナリオのすぐ隣に保存できます。そして、Apidog CLIを通じてCIで同じシナリオを実行することで、検証を静かに緩める変更があった場合、デプロイされるのではなくビルドが失敗します。試してみたい場合は、Apidogをダウンロードし、既存のエンドポイントにネガティブシナリオを1つ追加してみてください。
境界を明確にしてください。Apidogは設計、テスト、モック、ドキュメンテーションのツールです。ウェブアプリケーションファイアウォールを実行したり、ライブトラフィックをフィルタリングしたり、SIEMを置き換えたりするものではありませんし、テスト中の契約検証がすべてのエクスプロイトを捕捉するわけでもありません。Apidogがうまく機能するのは、契約を明示的にし、エンドポイントが何を受け入れるかについて正直であるように促すことで、「一度もチェックしなかった」というカテゴリが、本番環境であなたを驚かせるものでなくなるようにすることです。
よくある質問
ネガティブテストとファジングの違いは何ですか? ネガティブテストは、あなたが意図的に選択した一連の不正な入力を送信します。関心のある失敗ごとに1つです。ファジングは、あなたが思いつかなかったケースを見つけるために、大量のランダムまたは変異した入力を送信します。ネガティブテストから始めるのが良いでしょう。なぜなら、それらは高速で決定論的であり、CIで実行しやすいからです。自分の想像力を超える広範囲をカバーしたい場合にファジングを追加します。
これらのテストは本番環境に対して実行すべきですか? いいえ。ステージング環境または隔離された環境で実行してください。オーバーサイズペイロードやコマンドインジェクションのプローブなど、一部のケースはシステムに負荷をかけるように設計されており、バグが存在するとデータを変更する可能性があります。専用のテスト環境を使用することで、実際のユーザーへのリスクなしにテストを積極的に実行できます。
ファイアウォールやWAFがこれらを捕捉しないのですか? WAFは多層防御において有用ですが、アプリケーションが不正な入力を拒否する代替にはなりません。ルールはバイパスされる可能性があり、WAFはあなたのビジネスロジックを知ることはできません。これらのテストの目的は、エンドポイント自体が拒否することを証明することであり、完全に制御できないフィルターに依存することではありません。
エンドポイントごとにどれくらいのネガティブケースがあれば十分ですか? フィールドごとに、それが影響を受けうる失敗クラス(間違った型、範囲外、長すぎる、禁止されたフィールド、およびその形式に合う任意のインジェクション文字列)につき1つのケースを目安にしてください。これは通常、エンドポイントごとに数個のケースであり、数百ではありません。単純な数よりも、クラスのカバレッジが重要です。
スキーマ検証はインジェクションを完全に防ぎますか? いいえ、そしてそれは唯一の防御層であるべきではありません。厳格なスキーマは、不正な形式やサイズ過大な入力の大部分を排除し、予期せぬフィールドをブロックしますが、値はスキーマ的には有効であってもSQLインジェクションやテンプレートインジェクションである可能性があります。パラメータ化されたクエリ、安全な逆シリアル化、および出力エンコーディングを引き続き適用し、スキーマを使用してそれらの層が防御しなければならない表面積を縮小してください。
