aegis-api-security について
aegis-api-securityは、クライアントが意図された権限を超える原因となるAPIの設計・設定不備を探すリポジトリ監査スキルです。HTTP APIを対象に、静的な証拠、再現手順、重大度の根拠、フレームワークに適した修正案を示します。
主な機能
- 01リクエストバインドと一括代入の監査
- 02レート制限とページ上限の確認
- 03GraphQLとgRPCの公開範囲の確認
- 04REST、APIキー、エラー漏えいの点検
- 05WebSocketとSSEの認証評価
活用シーン
- 01APIリリース前のリポジトリ監査
- 02制限のないエンドポイントの特定
- 03スキーマやドキュメントの公開確認
- 04証拠と修正案を添えた指摘の記録
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ as-foss/ aegis-skills --skill aegis-api-securityインストール後、Claude Code で/aegis-api-securityを入力して読み込まれたか確認します。
同梱ファイルreferences/background.mdreferences/further-reading.md
詳細
スコープと目的
目的
対象リポジトリの HTTP 対応サーフェスを監査し、クライアントが意図された権限を超えられる API 固有の設計・設定上の問題を特定します。具体的には、サーバーが公開する意図のないフィールドへの書き込み、制限のないリソース消費、スキーマやドキュメントを通じた内部情報の列挙、トランスポートまたはチャネルの認証の回避、特権チャネルのクロスオリジン利用を可能にする問題です。各指摘には、ファイル名と行番号による証拠、再現手順(まず静的解析を実施)、根拠を添えた深刻度、フレームワークに即した修正方法を記載します。
対象範囲
| 分類 | 典型的な指摘 | 主な CWE |
|---|---|---|
| マスアサインメント/過剰なパラメーター送信 | リクエストボディのバインディングによって、クライアントが role、is_admin、price、user_id を ORM エンティティに設定できる | CWE-915 |
| レート制限と自動化対策 | 認証、検索、エクスポートのエンドポイントにスロットリングがない、ページネーションの上限がない、キーごとのクォータがない | CWE-770, CWE-799 |
| GraphQL の設定 | 本番環境でイントロスペクションが有効、Playground/GraphiQL が公開されている、深度/複雑度の制限がない、バッチ処理の悪用、フィールド候補による情報漏えい | CWE-200, CWE-400 |
| gRPC/protobuf | リフレクションサービスが公開されている、平文の h2c リスナーに外部から到達できる、RPC ごとの認証インターセプターがない | CWE-306, CWE-200 |
| REST の衛生管理 | _method オーバーライドによるシャドウイング、状態を変更する GET、廃止済みバージョンが稼働中、認証不要の OpenAPI ドキュメント、デバッグヘッダー、認証情報付きワイルドカード CORS、Content-Type の混同を入口とする問題、情報を漏らすエラー応答 | CWE-200, CWE-942 |
| API キー管理 | URL でキーを受け付ける、モバイル/SDK ビルドに共有キーが埋め込まれている、スコープのない全権キー、失効手順がない | CWE-798 |
| WebSocket/SSE チャネル | アップグレード時の Origin 検証がない、ハンドシェイク時のみの認証、ユーザーごとの ACL がないトピック | CWE-346, CWE-862 |
対象外(相互参照)
- 認証情報の総当たり攻撃に対するしきい値とアカウントロックアウトポリシーの詳細 → AUTHN モジュール。このモジュールではプラットフォームのスロットリング層のみを対象とします。
- ビジネスフロー内のオブジェクトレベル IDOR ロジック → AUTHZ モジュール。API の仕組みによって可能になる場合(GraphQL リゾルバー、サブスクリプショントピック)のみ、ここで扱います。
- XML Content-Type の混同による XXE ペイロードの構築 → DESER モジュール。受け付けているエンドポイントのみ指摘します。
- CSRF トークンの設計 → WEB モジュール。
_methodオーバーライドとの関係を記録し、トークンの分析は対象外とします。 - CORS の Origin 反映の全体像 → CONFIG モジュール。API 側のワイルドカードと認証情報の組み合わせに関する所見を入力情報として記録します。
- レスポンスやログへのシークレットの出力 → SECRETS モジュール。キーを URL に含めた場合のログ露出はここで指摘します。
- ネストされたクエリ爆弾による DoS の定量的な計算 → DOS モジュール。制限がないことを確認しますが、影響範囲の計算は行いません。
前提
リポジトリへの読み取り専用アクセスがあり、実行環境が利用できるとは限りません。動的な再現は任意とし、明示的に許可された環境でのみ実施できます。フレームワークの仕様を根拠とした静的な確認だけでも、報告に十分な証拠となります。
前提知識と用語
ゼロから始める人向けの基礎説明です。このモジュールで使う用語を、それぞれ一行で説明します。各分類についての詳しい平易な説明は、リポジトリの GUIDE.md 用語集にあります。
- mass assignment:フレームワークがリクエストボディ全体をオブジェクトにコピーし、クライアントが
is_adminなどのフィールドを設定できる仕組み - BOLA:リクエスト内の識別子を変更し、他のユーザーのオブジェクトにアクセスすること(詳しくは AUTHZ モジュールを参照)
- rate limit:一定期間内にクライアントが行えるリクエスト数の上限
- pagination cap:1 回の呼び出しで大量の行がダンプされるのを防ぐ、ページサイズの上限
- introspection / playground:攻撃者に API の全体像を渡す GraphQL の自己ドキュメント化機能
- resolver:各 GraphQL クエリに応答するフィールドごとの関数。すべての関数で個別に権限を確認する必要があります
- shadow API:ドキュメントに記載されていない稼働中のエンドポイント。レビューも見落とされています
- taint flow / source→sink:信頼されていないデータが入口から危険な関数へ移動する経路
考え方
1つのエンドポイントに潜む4つの暗黙的な契約
API ハンドラーは、クライアントから部分的にしか見えない4つの契約を組み合わせます。
- トランスポート:HTTP/1.1、HTTP/2、h2c、WebSocket アップグレード、SSE ストリーム。
- ルーティング/ミドルウェア:ハンドラー本体が実行される前に、どのガードが実行されたか。
- バインディング:サーバー上で、生のバイト列がどのように型付きオブジェクトになったか。
- シリアライズ:どのサーバーオブジェクトが再びバイト列になるか。
API に関する指摘の大半は、開発者がモデル化した内容と、フレームワークがこれらのレイヤーのいずれかで自動生成した内容との不一致です。マスアサインメントはバインディングとシリアライズに関係します。スロットリング、オリジン、チャネル認証に関する指摘は、トランスポートとミドルウェアに関係します。情報漏えいに関する指摘は、OpenAPI ファイル、GraphQL イントロスペクション、protobuf リフレクションといったスキーマの公開範囲に関係します。
フレームワークのマジックがデフォルトの姿勢を左右する
最新のスタックは、開発者の利便性を重視しています。リクエストボディ全体をモデルにバインドしたり、JSON からエンティティをハイドレートしたり、ツール向けに記述子を公開したりします。こうした利便性はいずれも、許容的なデフォルト設定につながります。
- 許容的なバインディングは、マスアサインメントを可能にします(CWE-915)。
- 許容的なスキーマ公開は、内部情報を漏えいさせます(イントロスペクション、
/openapi.json、gRPC リフレクション)。 - 許容的な利用には、リストサイズ、クエリの深さ、リクエスト頻度に対するデフォルトの上限がありません。
明示的なオプトアウトがないことだけでなく、安全性のための明示的なオプトイン(フィールドの許可リスト、リミッター、イントロスペクションの無効化、認証インターセプター)を監査してください。証拠が確認できるまでは、デフォルト設定は許容的だと考えてください。
コストの非対称性がスロットリングの問題を引き起こす
すべてのエンドポイントには、サーバー側のコストとクライアント側の料金があります。結合を伴う検索、エクスポート、パスワードリセットメールの送信、SMS の送信はコストを増大させます。ページネーションの上限とスロットリングは、そのコストを測る計器です。計器がなければ、1回のリクエストが自動化された1万回のリクエストに変わり、列挙、ブルートフォース、スクレイピング、金銭的損失を招きます。コストの高い各エンドポイントについて、「スクリプトがこれを1万回呼び出すのを防ぐものは何か」と問い、評価してください。
GraphQL は境界をリゾルバーに集約する
1つの URL から、クライアントが形作るクエリで、何千ものリゾルバーにアクセスできます。リゾルバーが「スキーマは内部向けだから」とオブジェクトレベルのチェックを省略しているなら、/graphql の境界で認証しても何も保護できません。スキーマ自体がオブジェクトグラフの公開マップであり、クエリの深さは攻撃者が選べるループカウンターです。ミドルウェアで保護されていると確認できるまでは、すべてのリゾルバーを未認証のハンドラーとして扱い、イントロスペクション、プレイグラウンド、フィールド候補はドキュメントの漏えいとして扱ってください。
gRPC では同じ問題が1つ下のレイヤーに移る
protobuf は JSON バインディングに、リフレクションは Swagger に、インターセプターはミドルウェアに取って代わります。同じ点を確認してください。どの RPC を誰が呼び出せるのか(インターセプターで適用されるメソッドごとの認証メタデータがあるのか、それともまったくないのか)。サイズ超過または未知のフィールドを多数含むメッセージは、メモリやストレージにどのような影響を与えるのか。クラスター外部から平文の h2c ポートにアクセスできるのか。
チャネルはハンドシェイクより長く存続する
WebSocket と SSE の接続は長時間存続し、メッセージを通じて複数のアクションを多重化します。ハンドシェイク時だけ認証し、メッセージごとの認可を行わない場合、接続中のソケットはどれも、接続期間中ずっとセッションの権限を引き継ぎます。アップグレード時の Origin チェックも行われないと、被害者のブラウザーで悪意ある Web ページがクロスオリジンでそのソケットを開き、被害者の Cookie を利用します。
確認する項目
一括割り当て/過剰なデータ送信
- リクエストボディを取り込む箇所(コントローラーアクション、GraphQL ミューテーションリゾルバー、gRPC ハンドラー)をすべて特定します。バインドされる型が何か、ORM/ドメインエンティティなのか専用のリクエスト DTO なのかを確認します。
- ORM エンティティへの直接バインドを検出します。許可するフィールドが広すぎる DRF
ModelSerializer、create()/findByIdAndUpdate()にそのまま渡される Mongoose モデル、create(req.body)を使う Sequelize/Prisma、Model::create($request->all())を使う Eloquent、permitを使わないModel.new(params)を使う Rails、JPA エンティティにバインドされる Spring の@ModelAttribute、UserがDbSet<>エンティティである場合に使われる ASP.NET Core の[FromBody] User user、共有モデルに対してjson.NewDecoder(r.Body).Decode(&domainStruct)を行う Go のコードを確認します。 - シリアライザー/スキーマを検索し、許可リストがない箇所を洗い出します。DRF の
fields = "__all__"、whitelist: trueを設定していないValidationPipe()と併用される NestJS DTO、$fillableが未設定(または$guarded = [])の Laravel モデル、extra="allow"に設定された Pydantic モデルを確認します。 - バインド対象の各モデルについて、設定可能な機密フィールドの候補をすべて列挙します。
role、roles、is_admin、admin、permissions、price、amount、balance、discount、status、verified、email_verified、user_id、owner_id、account_id、tenant_id、created_at、password_hash、creditsが対象です。各候補について、クライアント向けのどの経路でバインドされるかを追跡します。 - 特に PATCH/PUT のマージ処理を調べます。
Object.assign(entity, req.body)、req.bodyのスプレッドを更新オブジェクトに取り込む処理、ペイロードのキーを反復する Python のsetattr、paramsを使う Ruby のmerge!、エンティティに対する EF Core のCurrentValues.SetValues(dto)、dict.update(payload)を確認します。 - 同じ問題がより深い階層に連鎖するネスト/埋め込み書き込みも確認します。Rails の
accepts_nested_attributes_for、書き込み可能なネストされた DRF シリアライザー、リクエストボディからハイドレートされる Mongoose のサブドキュメント配列が対象です。 - 裏付けとして、レスポンスとの対称性を確認します。GET で返されるフィールドに、文書化された書き込み経路がない場合、報告する前に、文書化されていない経路でそのフィールドがバインドされないことを確認します。
レート制限と自動化対策
- ルートグループごとにミドルウェアチェーンを再構成し、レート制限が適用されるルートと適用されないルートを記録します。レート制限のない認証(ログイン、登録、パスワードリセット、OTP)、検索、エクスポート、レポート生成、メール/SMS 送信を起動する処理、支払い見積もりの各エンドポイントを優先して確認します。
- レート制限のキー設定を確認します。
X-Forwarded-Forをクライアントが制御できる場合、IP のみをキーにすると、プロキシの背後で機能しません(app.set('trust proxy', true))。ユーザー ID/API キーを優先し、IP はフォールバックとして使用します。 - すべてのコレクションエンドポイントを列挙し、サーバー側に厳格な上限があることを確認します。最大ページサイズの制限、上限付きのオフセット/ウィンドウ、カーソルの上限、
SELECT ... LIMIT、上限のない結果を返す.all()/.find({})/FindAllがないこと、単一リクエストでテーブル全体をストリーミングするエクスポートエンドポイントがないことを確認します。 - 従量制 API では、API キーごとのクォータが存在し、サーバー側で適用されていることを確認します(クォータミドルウェア、プラン階層テーブル)。ドキュメントで告知されているだけでは不十分です。
- 不正利用の影響を受けやすいフローで、CAPTCHA/プルーフ・オブ・ワークなど、同等の利用抑制策があるかを確認します。対象はサインアップ、投票、一括招待、特典/ギフトカードの引き換えです。
- 1 回のリクエストを高コストにする要因を記録します。すべてのデータを先行読み込みする処理(
include: [...all]、populate()、select_relatedの連鎖)、巨大なテーブルを横断する検索結合、PDF/レポート生成が対象です。
GraphQL の公開範囲
- サーバーの起動処理(apollo-server、
@nestjs/graphql、graphql-yoga、strawberry-graphql、graphene、gqlgen、graphql-go)を特定します。コード内に設定が存在するかだけでなく、本番環境での introspection とランディングページ/プレイグラウンドのフラグの値を確認します。 - クエリの検証制御を確認します。深さ制限、複雑度/コスト分析、エイリアス数の制限(対応している場合)、エラー内のフィールド候補表示の無効化を調べます。
- オブジェクトレベルの認可漏れがないか、リゾルバーを抜き取りで確認します。
id/uuid引数を受け取り、所有者やテナントのチェックなしにオブジェクトを取得して返すクエリ/ミューテーションを調べます。特に「internal schema」などのコメントがある箇所に注意します。 - 静的にバッチ処理の設定をテストします。エンドポイントは配列形式の本文(
[{"query":"..."},...])を受け付け、1 回のリクエストで多数の操作を実行できますか?AutomaticPersistedQuery は登録済み操作の許可リストとともに使用されていますか。それとも任意のハッシュを受け付けますか? - エラーの整形処理を調べます。不明なフィールドに対して「Did you mean」形式の候補が表示されますか?
errors[].extensionsにスタックトレースや内部パスが含まれていますか? - サブスクリプションでは、購読時と各イベントの配信時の両方で認証を行います。トピック/チャネルの ACL も確認します。
gRPC/Protobuf
- リフレクションの登録箇所を特定し、環境による制御があるか(開発環境のみか、常時有効か)を確認します。
- リスナーの認証情報を特定します。平文の
NewServer()/h2c か、TLS かを確認します。リッスンポートを Dockerfile のEXPOSE、Kubernetes マニフェスト、ロードバランサー設定と照合し、外部から到達可能かを判断します。 - 認証の適用を確認します。メタデータ(トークン、API キー)を検証する単項/ストリームインターセプターが、登録されたすべてのサービスに適用されていますか。それとも、インターセプターなしで登録されたサービスがありますか?意図的な例外(ヘルスチェック)が限定的かどうかも確認します。
- メッセージの堅牢性を評価します。適切な
MaxRecvMsgSize(math.MaxInt64ではないか)、型 URL に基づくディスパッチでのgoogle.protobuf.Anyの使用、永続化パスでの未知フィールドの処理、保存前の enum 検証を確認します。 - ゲートウェイの公開範囲を確認します(grpc-gateway、Envoy の JSON トランスコーディング)。HTTP パスではネイティブ RPC と同じ認証が適用されていますか?
REST の衛生管理
- 動詞とルートのシャドーイング:
_methodのフォーム/クエリパラメーターとX-HTTP-Method-Overrideミドルウェアを確認します。オーバーライドされた動詞が許可リストに制限され、セッション認証の場合は CSRF 対策が施されていることを確認します。 - GET による状態変更:ルートテーブルを調べ、変更を示す動詞(
delete、update、create、reset、send、confirm、approve)を含む GET パスを探します。 - バージョンの乱立:複数の
/vNプレフィックスが同時にマウントされていないか、非推奨のバージョンにまだトラフィックが届いていないかを確認します。セキュリティミドルウェア(認可ガード、リミッター)が最新のルーターにしか接続されず、/v1が無防備なまま動作していないかも確認します。 - ドキュメントの公開状態:
/swagger、/swagger-ui.html、/api-docs、/openapi.json、HTML の Accept ヘッダーを使用した/graphql、redoc ページ、drf-yasg、springdoc actuator のマッピングを確認し、本番環境の構成でそれぞれに認証が必要か確認します。また、公開された仕様に記載されている内部専用ホストやエンドポイントがないか、仕様の内容も確認します。 - デバッグ/内部ヘッダー:
X-Powered-By、X-AspNet-Version、Symfony のX-Debug-Tokenとアクセス可能な/_profiler、Serverバナー、内部情報を列挙するAccess-Control-Expose-Headersを確認します。 - ルートグループ間のミドルウェア適用状況:
app.use(...)の順序を比較し、最新のグループでは適用されている認証またはレート制限ミドルウェアが適用されていないグループ(/internal、/webhooks、レガシーな/v1)を探します。 - コンテンツタイプの処理:XML に対応するエンドポイント(エントリーポイントのみ。ペイロードの作成は DESER の対象です)を特定します。コンテンツタイプに基づく CSRF チェックを回避できる
text/plainを受け入れるルートや、リクエストボディを解析する GET ハンドラーも探します。 - コンテンツネゴシエーション:Accept ヘッダーに応じたシリアライザー(DRF の
renderer_classes、ASP.NET の出力フォーマッター、Rails のレスポンダー)が、デフォルトの JSON 表示より詳細なシリアライズ結果を公開していないか確認します(例:?format=xmlで関連データがダンプされる)。 - エラーエンベロープ:エラーハンドラーが統一されているか確認します。エラーボディにスタックトレース、クラス名、内部ホスト名が漏れていないか、環境フラグで切り替わる詳細なエラー出力が有効なままになっていないかを調べます。
- シャドー API の検出:実装されたルートテーブルと OpenAPI 仕様を照合します。コード内には存在するものの公開仕様にはないエンドポイントは、文書化されていない攻撃対象領域です。不一致の両側について、ファイル名と行番号の証拠を記録します。
- ゲートウェイ層のアクセスログ分析マーカー:API アクセスログが何らかの分析/異常レビュー(キーごとのボリューム基準、エラー率の急増など)に利用されているか確認します。ログを収集しても分析しなければ、不正利用のパターンが見えないままになります。
API キー管理
- キーの受け入れ箇所を特定します。クエリ文字列パラメーター(
api_key、access_token、token)とヘッダー(Authorization、X-API-Key)を確認します。URL での受け渡しには注意を促してください。URL はプロキシログ、ブラウザー履歴、リファラーヘッダーに残ります。 - リポジトリやモバイル/SDK ディレクトリを検索し、SDK の初期化箇所に埋め込まれた静的キーを探します。プロバイダーのプレフィックスパターンごとに分類し、クライアントに配布可能な秘密情報かどうかを判断します。
- スコープ設定を評価します。1 つのキーですべてのエンドポイントにアクセスできる(万能キー)状態になっていないか確認します。キーのレコードに紐づくスコープ/権限チェックと、キーごとのクォータ適用を探します。
- ライフサイクルを評価します。キーのステータス列、有効期限のタイムスタンプ、ローテーション用エンドポイント、失効フロー、発行監査証跡を確認します。失効経路がないこと自体が調査結果になります。
- 認証不要の公開 API:サインアップの手間がなく、匿名ユーザーに対するデフォルトの制限が緩い場合、不正利用の可能性が高まります。デフォルト値を記録します。
- ライフサイクルの深度:キーのレコードに作成日時のタイムスタンプと最終使用状況の追跡を含めることで、休眠キーを検出できます。使用状況のテレメトリーがないキーでは、古い認証情報をクリーンアップできません。
WebSocket / SSE
- アップグレード経路:サーバーの作成箇所(ws、socket.io、SignalR、gorilla/websocket、ActionCable、Phoenix Channels、Django Channels)を特定します。アップグレード時に Origin/Host の検証が適用されていることを確認します(ws の
verifyClient、socket.io のcorsオプション、SignalR のAllowedOrigins、CheckOrigin、OriginValidator)。 - ハンドシェイク後の認証:接続時にのみ ID が確立されていませんか?メッセージごとに、メッセージの種類やアクションが認可されていますか?ハンドシェイクから取得した ID とクライアント提供の ID だけを使ってデータを変更する
socket.on('action', ...)ハンドラーを探します。 - サブスクリプションのトピック/ルーム:ルーム/チャネルへの参加時にユーザーごとの ACL が検証されているか、クライアント提供のトピック名をそのまま受け入れていないか(
socket.join(req.body.room))を確認します。 - SSE:
text/event-streamエンドポイントを特定し、ストリームリクエスト自体の認証と、Last-Event-IDの再生動作を確認します(再生によって他のテナントのイベントが漏洩してはなりません)。 - サービス間の信頼:ソケットは HTTP ミドルウェアのチェーンを通らないことがよくあります。「接続は信頼できる」と決めつけず、ソケットハンドラーで認可を再適用していることを確認します。
確認箇所
フレームワーク別の場所一覧
| スタック | バインディング/シリアライザーのファイル | ルート/ミドルウェアのファイル | 備考 |
|---|---|---|---|
| Express/Fastify | ルートと同じ場所に配置されたハンドラー | app.js、server.ts、routes/*.js、Fastify の schema: キー | Fastify の JSON Schema における additionalProperties:false はバインディングの動作に影響します。各ルートのスキーマを確認してください |
| NestJS | *.dto.ts、entities/*.entity.ts | main.ts(グローバルパイプ/ガード)、*.controller.ts、app.module.ts | グローバルとコントローラーレベルの ValidationPipe、ガードの適用順序 |
| Django REST Framework | serializers.py、models.py | urls.py、views.py、settings.py の REST_FRAMEWORK ブロック | デフォルトのスロットルクラスは設定に記述されています |
| FastAPI/Flask-RESTful | Pydantic スキーマ schemas.py | main.py、api/*.py、Flask の app = Flask(...) デコレーターチェーン | Pydantic の extra 設定で不明なフィールドの動作が決まります |
| Spring MVC/WebFlux | エンティティクラス、プロジェクション DTO、@ControllerAdvice | @SpringBootApplication のメインクラス、WebSecurityConfigurerAdapter/SecurityFilterChain Bean、コントローラー | springdoc.* プロパティでドキュメントの公開が制御されます |
| ASP.NET Core Web API | Models/、DTOs/ | Startup.cs/Program.cs、Controllers/*.cs | パイプライン内のミドルウェアの順序、環境に応じた Swashbuckle の UseSwaggerUI の有効化 |
| Laravel/Lumen | app/Http/Requests/*、app/Models/*($fillable) | routes/api.php、app/Http/Kernel.php($middleware、$middlewareGroups) | 各ルートグループに throttle: ミドルウェアのエイリアスがあるか |
| Rails API モード | app/models/*、app/serializers/* | config/routes.rb、config/application.rb、Rack::Attack のイニシャライザー | API モードでは ActionController::Base.allow_forgery_protection は関係ありません。コントローラー内の strong params を確認します |
| Go chi/gin/echo | 構造体をバインドするハンドラーファイル | main.go/cmd/*/main.go のルーター設定、ミドルウェアの登録 | 自動バインディングの仕組みはありません。共有ドメイン構造体へのデコードがリスクです |
| GraphQL(apollo/yoga/strawberry/graphene) | typeDefs、resolvers.*、スキーマファイル | サーバーのブートストラップファイル(apollo-server コンストラクター、yoga の設定、strawberry の extensions=[...]) | 検証ルールはここでのみ設定されます |
| gRPC | *.proto(生成されたスタブは対象外) | サーバーのセットアップ(grpc.NewServer(...)、Python の grpc.server(...)、Java の ServerBuilder) | インターセプターとリフレクションはここで登録されます |
API の設定に関わるファイル
settings.py:REST_FRAMEWORK辞書(スロットル、レンダラー、パーサー)、DEBUG、ミドルウェア一覧。.env、config/*.yaml、application.properties/yml、appsettings.json:イントロスペクションのフラグ、Swagger の有効化、レート制限の値、信頼するプロキシの設定。Dockerfile、docker-compose.yml、k8s/:外部に公開されるポート(平文 gRPC を含む)。- リバースプロキシ(
nginx.conf、Envoy の設定):コードの問題としてフラグを立てる前に、プラットフォーム層のレート制限でスロットリングの指摘に対応できる場合があります。
検索順序
ルートテーブルから始めて、すべてのエンドポイントを洗い出します。次に、取り込みポイントを調べてバインディングの状態を確認し、その後、ブートストラップ/設定ファイルでグローバルフラグ(パイプ、リミッター、イントロスペクション)を確認してから、各エンドポイントを調べて不足している制御を特定します。この順序に従うことで、保護されたルートグループをサービス全体が保護されていると誤認するのを防げます。
パターンとシグネチャ
すべての正規表現は ripgrep(rg)と Rust の正規表現クレートで実行されます。後読みや後方参照は使用できません。リポジトリのルートから実行し、報告する前にヒットを手動でトリアージしてください。
マスアサインメントのシグネチャ
fields\s*=\s*["']__all__["']
is_admin や password_hash を含め、すべてのモデル列を公開する DRF シリアライザーです。
\$request->all\(\)
Laravel/Lumen の未加工リクエスト配列です。各ヒットについて、create()/update()/fill() へのデータフローを確認してください。
->(create|update|fill|firstOrCreate|updateOrCreate)\(\$request->all\(\)\)
Eloquent における直接的な過剰ポストのシンクです。
\.new\(params\[|\.create\(params\[|\.update\(params\[
フィルタリングされていないパラメーターから Rails モデルを構築します。同じアクションの上流に .permit(...) があることを確認してください。
(Object\.assign|merge|extend)\(\s*[A-Za-z_$][\w$.]*\s*,\s*req\.body
Express/Fastify で、永続化オブジェクトに任意のキーをマージします。
\.\.\.req\.body
クライアントの本文を更新ペイロードまたはコンストラクターオプションに展開します。
\.(create|findByIdAndUpdate|findOneAndUpdate|updateOne|bulkWrite)\(\s*req\.body\b
リクエスト本文全体を使用する Mongoose の書き込みシンクです。
ValidationPipe\(\)
デフォルトオプションの NestJS パイプです。不明なプロパティがそのまま通過します。
@ModelAttribute|setAllowedFields|ignoreUnknownFields
Spring のデータバインディングの対象と、(まれな)安全策です。エンティティクラスにバインドされた @ModelAttribute を指摘してください。
FromBody\]\s+[A-Za-z_][A-Za-z0-9_]*\s+\w+
ASP.NET Core で本文にバインドされるパラメーターです。バインドされた型名を DbSet<...> 宣言と照合して、エンティティへのバインディングを特定してください。
extra\s*[=:]\s*["'](allow|ignore)["']?
Pydantic の設定です。allow は追加フィールドを受け入れて保存し、通常の ignore はそれらを破棄するため、書き込みでは安全です。
setattr\s*\(\s*[a-z_]+\s*,\s*(k|key|field|attr|name)\s*,|merge!\(|SetValues\(|\.update\(payload\)|\.update\(data\)
Python/Ruby/.NET/JS における動的キーの PATCH マージシンクです。
レート制限とページネーションのシグネチャ
まずリミッターの有無を一覧化し、次にエンドポイントの一覧と照合します。
(?i)(rate.?limit|throttle|rack.?attack|slowapi|flask_limiter|django_ratelimit|addratelimiter|ratelimiting|express-rate-limit|nestjs/throttler)
TooManyRequests|StatusTooManyRequests|HttpStatusCode\.TooManyRequests
上限のないコレクションシンク:
\.all\(\)|\.find\(\s*\{\s*\}\s*\)|findAll\(\)|FindAll\(\)|SELECT\s+\*\s+FROM
リストルートに想定されるページネーションパラメーター(コレクションルートにこれらがない場合は指摘対象です):
[?&](page|per_page|pageSize|limit|offset|cursor|after|before)=
IP アドレスをキーとするリミッターでのプロキシ信頼に関する落とし穴:
trust proxy|trustProxy|X-Forwarded-For
GraphQL のシグネチャ
introspection\s*:|GRAPHQL_INTROSPECTION|disableIntrospection|DisableIntrospection|introspectionEnabled
graphiql|playground|landingPage|LandingPage|GraphiQL
深度/複雑度の制御(設定があれば良好です。GraphQL のブートストラップ付近にない場合は指摘対象です):
depthLimit|max_depth|maxDepth|QueryDepthLimiter|complexity|ComplexityLimit|costAnalysis|validationRules
バッチ処理/永続クエリの仕組み:
PersistedQuery|persistedQueries|APQ|shouldPersistedQuery|batching
gRPC のシグネチャ
reflection\.Register|enable_server_reflection|ProtoReflectionService|RegisterReflectionService
平文リスナー:
insecure\.NewCredentials|InsecureServerCredentials|WithTransportCredentials\(insecure|h2c
認証インターセプターの有無(サーバー構築付近にない場合は指摘対象です):
UnaryInterceptor|StreamInterceptor|ServerInterceptors|ServerInterceptor|AddUnaryInterceptors|AddStreamInterceptors
メッセージサイズの設定:
MaxRecvMsgSize|MaxReceiveMessageSize|max_receive_message_length
REST の衛生管理に関するシグネチャ
メソッドオーバーライドの仕組み:
_method|methodOverride|MethodOverride|X-HTTP-Method-Override|HttpMethodOverride
GET での状態変更(Express 形式の場合。Rails/Django/Flask のルートテーブルではスキャンを手動で繰り返してください):
\.(get|GET)\(\s*["'][^"']*(delete|remove|destroy|update|edit|create|reset|send|confirm|approve)
バージョンの乱立:
/api/v[0-9]|["']/v[0-9]+/
ドキュメントの公開:
swagger-ui|swagger\.json|openapi\.json|api-docs|UseSwaggerUI|drf_yasg|drf-spectacular|flask_swagger_ui|springdoc
デバッグ/内部ヘッダーの送信:
X-Powered-By|x-aspnet-version|x-debug-token|_profiler|exposeHeaders|Access-Control-Expose-Headers
CORS のワイルドカードとオリジンの反映:
Access-Control-Allow-Origin["']?\s*[,:]\s*["']?\*
origin\s*:\s*true|callback\(null,\s*req\.headers\.origin|SetIsOriginAllowed\(|AllowCredentials\(\)|credentials\s*:\s*true
Content-Type の取り違えにつながるエントリーポイント(ペイロードの作成 -> DESER モジュール):
xml-bodyparser|lxml\.etree|etree\.fromstring|fromstring\(|JAXBContext|JacksonXml|XmlSerializer|application/xml
エラーエンベロープからの情報漏えい:
err\.stack|error\.stack|traceback\.format_exc|printStackTrace|DEBUG\s*=\s*True|includeErrorDetails|DetailedErrors|includeStacktrace
API キーのシグネチャ
URL/クエリから受け入れるキー(ログに露出する転送方式):
request\.args\.get\(["'](api_key|apikey|token)["']\)|query\.Get\("api_key"\)|params\[:(api_key|token)\]|Request\.Query\[["']api_key
ハードコードされた認証情報の形式(広く知られている公開プレフィックス):
AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z_\-]{35}|sk_live_[0-9a-zA-Z]{16,}|ghp_[0-9A-Za-z]{36}|xox[baprs]-[0-9A-Za-z\-]{10,}
スコープの適用を示すマーカー(存在すれば良好です):
scopes?\s*[=:]|hasScope|scope_required|check_scopes|permissions\.includes
WebSocket/SSE のシグネチャ
アップグレード処理とオリジンの検証:
WebSocketServer|io\.on\(["']connection|handleUpgrade|verifyClient|CheckOrigin|AllowedOrigins|OriginValidator|allowed_origins|origins
メッセージごとの認可が必要な、メッセージ単位のアクションハンドラー:
socket\.on\(|conn\.on\(|OnConnected|receive\(|async_receive
ACL チェックが必要なルーム/トピックへの参加:
socket\.join\(|join_room|groups\.add|AddToGroupAsync|channels\.subscribe
SSE の対象:
text/event-stream|EventSource|Last-Event-ID
ペイロード早見表
一括代入のリクエストボディ(登録、プロフィール、注文エンドポイントへの POST/PUT/PATCH):
{"email":"[email protected]","password":"Str0ng!passphrase","is_admin":true}
{"username":"a","price":0,"user_id":2,"role":"admin"}
{"nickname":"x","tenant_id":1,"balance":999999,"email_verified":true}
PATCH のマージプローブ(部分更新に任意のキーを含めます):
{"role":"admin","permissions":["*"],"status":"active"}
GraphQL プローブ:
- 簡易イントロスペクション:
{"query":"{__schema{types{name}}}"}—"types"を含む型名のリストが JSON ボディに返されれば、イントロスペクションが有効です。 - 存在確認プローブ:
{"query":"{__typename}"}— GraphQL エンドポイントの手軽な稼働確認です。 - フィールド候補プローブ:
{"query":"{users{usrnme}}"}— "Did you mean" を含むエラーによって、スキーマ名が漏えいします。 - 深度爆弾の形状(説明のみ。巨大なペイロードは送信しないでください):選択セットを 20~100 階層にネストし、各階層で別のオブジェクトのコネクションフィールドを選択するクエリです。例:
user -> friends -> friends -> friends ...。各階層でエイリアスを重複させることで、さらに増幅することもできます。動的テストを検討する前に、深度/複雑度の制限がないことを静的に確認してください。 - バッチ処理プローブ:
[{"query":"{__typename}"},{"query":"{__typename}"}]— 1 つのレスポンスに 2 つの回答があれば、オペレーションのバッチ処理が有効です。
ページネーション不在のプローブ:
?page=999999&limit=all
?limit=1000000
?offset=0&count=-1
?perPage=99999999
これらに対して、全データを返す(400 エラーになったり、上限で制限されたりしない)エンドポイントは、ダンプを一度に返すエンドポイントです。
_method オーバーライドの例:
POST /users/1/delete route registered for GET only
form body: _method=DELETE&_token=<csrf> Rails/Laravel form override
header: X-HTTP-Method-Override: PUT sent against POST /api/items/1
query: /api/items/1?_method=DELETE query-string variant
WebSocket のクロスオリジンハンドシェイクプローブ:
curl -i -N -H "Connection: Upgrade" -H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
-H "Origin: https://evil.example" https://host/ws
異なる Origin からのリクエストに対して HTTP/1.1 101 Switching Protocols が返される場合、アップグレード時の Origin 検証が行われていません。
汚染追跡の指針
クライアントが指定したすべてのキーと値のペアを、トランスポート境界で汚染されたものとして扱い、永続化シンクまで追跡してください。一括代入で注目する汚染対象は、値だけでなくキー名です。攻撃者が制御するのは、属性に何を格納するかだけでなく、どの属性に書き込むかです。
ソース/シンク表
| 言語/スタック | ソース(クライアントが制御可能) | シンク(永続化) |
|---|---|---|
| Node/Express/Fastify | req.body、req.query、req.params、Fastify の解析済みボディ | Model.create()、findByIdAndUpdate()、Object.assign(entity, ...)、Knex の update(payload)、Prisma の create(data) |
| NestJS | @Body() を付与した DTO パラメーター、ガード内の生の request.body | リポジトリの .save(partialEntity)、TypeORM の preload()/merge() |
| Python DRF | request.data、シリアライザーの入力 | serializer.save()、Model.objects.create(**validated_data) |
| Python Flask/FastAPI | request.get_json()、Pydantic モデルのフィールド(model_dump()) | db.session.add(obj)、SQLAlchemy の update(values) |
| Ruby Rails | params ハッシュ | Model.new(params)、update(params)、assign_attributes、merge! |
| PHP Laravel | $request->all()、$request->input() | ::create($array)、->fill($array)、->forceFill() |
| Java Spring | アノテーション付きパラメーターへのフォーム/クエリのバインド、@RequestBody DTO | JPA リポジトリの .save(entity)、setData(...) |
| C# ASP.NET | [FromBody] モデル、TryUpdateModelAsync | EF Core の _context.Add()、CurrentValues.SetValues() |
| Go | json.NewDecoder(r.Body).Decode(&v)、gin の c.ShouldBindJSON(&v) | GORM の .Create(v)、.Updates(map)、構造体フィールドから構築した sqlx の insert |
| GraphQL | リゾルバー (root, args, ctx)、ネストされた入力オブジェクト | ホスト言語と同じ ORM シンク |
手順
- ソースを起点にします。「調査箇所」に記載されたすべてのボディ取り込みポイントを列挙します。
- 順に追跡します。バインドされたオブジェクトは、途中にフィールド削減ステップを挟まずにシンクテーブルの行へ到達しますか?
- フィールド削減ステップのうち、フローを遮断するもの:明示的な許可リスト(
permit、fields = [...]、whitelist:true、$fillable、投影 DTO、pick()ヘルパー)。それ以外の変換(バリデーション、デフォルト値、マッピングライブラリ)は、名前付きフィールドを持つ新しいオブジェクトを構築するのでない限り、フローを遮断しません。 - シンクで、書き込み可能な属性セットと機密性の高い候補リスト(「確認項目」にある role/is_admin/price/user_id など)を比較します。
- GraphQL では、さらにリゾルバーごとに追跡します。
idに類する引数を持つ各リゾルバーが、それぞれ独立したソースからシンクへのパスです。リゾルバーまたはそのデータローダー内の所有権述語を確認します。 - gRPC では、ソースは proto メッセージのフィールドです。メッセージフィールドがエンティティフィールドに直接設定されるハンドラーのコードパス、特に部分マスクを適用する更新 RPC を追跡します。
フローを遮断する構文の参照
// VULNERABLE - spread preserves attacker keys
const user = await User.create({ ...req.body });
// FIXED - explicit pick
const { email, password, nickname } = req.body;
const user = await User.create({ email, password, nickname });
# VULNERABLE - dynamic kwargs
User.objects.create(**request.data)
# FIXED - named fields
User.objects.create(email=request.data["email"], nickname=request.data.get("nickname", ""))
悪用と再現
まず静的に確認します。動的な手順は、明示的にテストを許可されている環境でのみ実行してください。件数は意図的に少なく抑えています。
マスアサインメントによる権限昇格
- 登録またはプロフィール更新のエンドポイントと、それがバインドするエンティティを特定します(静的な手順)。
- 通常の登録リクエストに、機密フィールドを1つ追加して送信します。
curl -s -X POST https://host/api/register -H 'Content-Type: application/json' \
-d '{"email":"[email protected]","password":"Str0ng!passphrase","is_admin":true}'
- そのユーザーとして認証し、管理者専用のエンドポイント(例:
GET /api/admin/users)をリクエストします。 - 真陽性の場合に期待される観測結果:機密フィールドが黙って受け入れられ(未知のプロパティを示す 400 は返されず)、管理者エンドポイントが 401/403 ではなく 200 を返します。サーバーが未知のフィールドを 400/422 で拒否する場合は、制御が機能していることを記録します。
- 権限を主張せずに検証する方法:
"role":"admin"を含む PATCH の前後でGET /api/meを比較するか、未知のキーが受け入れられることと、コードレベルのシンクを確認するだけでも、報告には十分です。
PATCH による任意キーのマージ
curl -s -X PATCH https://host/api/users/me -H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" -d '{"role":"admin","tenant_id":1}'
期待される観測結果:注入したフィールドをレスポンスボディに含めて 200 OK が返ります。
スロットリングの不在
- 認証エンドポイントを選びます(ログインが理想的です)。
for i in $(seq 1 20); do
curl -s -o /dev/null -w "%{http_code}\n" -X POST https://host/api/login \
-H 'Content-Type: application/json' -d '{"email":"[email protected]","password":"wrong"}'
done | sort | uniq -c
- スロットリングがない場合に期待される観測結果:連続する 20 件のレスポンスがすべて
200/401で、429/403によるスロットリングがありません。429が1件でもあればリミッターが存在します。次に、そのキー設定と適用範囲を確認します。
ページネーションの不在/一括ダンプ
curl -s "https://host/api/users?page=999999&limit=all" | head -c 400
curl -s "https://host/api/users" | wc -c
期待される観測結果:データ全体のペイロードが返されます(400 ではなく、最大ページサイズにも制限されません)。また、レスポンスサイズがテーブル内のデータ量に応じて増加します。
GraphQL のイントロスペクション
curl -s https://host/graphql -H 'Content-Type: application/json' \
-d '{"query":"{__schema{types{name}}}"}'
期待される観測結果:スキーマの型名(Query、Mutation、カスタム型など)を列挙した types 配列を含む JSON が返されます。「GraphQL introspection is disabled」のようなエラーは陰性ケースです。制御が機能していることを記録します。
GraphQL Playground/GraphiQL の公開
curl -s https://host/graphql -H 'Accept: text/html' | grep -io 'graphiql\|playground'
期待される観測結果:HTML のランディングページを示すマーカー(GraphiQL、Playground、ApolloServerPluginLandingPage の出力)が含まれます。HTML ではなく JSON エラーが返る場合は陰性ケースです。
フィールド候補の漏えい
curl -s https://host/graphql -H 'Content-Type: application/json' \
-d '{"query":"{users{usrnme}}"}'
期待される観測結果:errors[0].message に「Did you mean ...?」と実際のフィールド名が含まれます。
gRPC リフレクション
grpcurl -plaintext host:50051 list
期待される観測結果:認証情報なしでサービス一覧がダンプされます。接続拒否/TLS 必須の場合は、平文通信に関する所見を別途記録します。
メソッドオーバーライドによるシャドーイング
curl -s -X POST "https://host/api/items/1" -H 'Content-Type: application/x-www-form-urlencoded' \
--data '_method=DELETE' -b "session=$SESSION"
期待される観測結果:POST 専用ルート経由で項目が削除されます。どのオーバーライドミドルウェアがマウントされているか、またオーバーライド対象の動詞が許可リストに含まれ、CSRF チェックが行われるかを静的に確認します。
OpenAPI/Swagger の公開
次の順に確認します:GET /swagger-ui.html、/swagger/index.html、/swagger/v1/swagger.json、/api-docs、/openapi.json、/docs、/graphql(HTML の Accept)。期待される観測結果:認証なしで、対話型ドキュメントの UI を含む HTML または生の仕様 JSON が返されます。仕様の内容を照合し、内部ホスト/エンドポイントが含まれていないか確認します。
WebSocket クロスオリジンアップグレード
Payload Cheat Sheet のハンドシェイクプローブを Origin: https://evil.example とともに実行します。拒否された場合に期待される結果は 101 Switching Protocols です。その後、認可されている場合は、認証なしでアクションメッセージを送信します。成功すれば、メッセージごとの認可が欠落していることを証明できます。
URL 内の API キーの確認
静的解析:クエリ文字列でのキー受け入れ箇所を探します。動的な裏付け:?api_key=... を指定してエンドポイントを呼び出し、アクセスログの設定を調べます(nginx の $request_uri ログ記録はデフォルトで有効です)。キーはログの出力先に記録されます。
修正
シリアライザー/DTO のフィールド許可リスト
| フレームワーク | リスクを示す記述 | 正しいパターン |
|---|---|---|
| Django REST Framework | fields = "__all__" | fields = ("email", "nickname") のように明示し、計算列には read_only=True を指定します |
| NestJS | ValidationPipe() のみ | new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }) |
| Spring MVC | エンティティへの @ModelAttribute | ユースケースごとに投影 DTO を用意します。@InitBinder で setAllowedFields を設定し、Jackson の未知プロパティの失敗を有効にします(spring.jackson.deserialization.fail-on-unknown-properties=true) |
| Rails API | Model.new(params) | params.require(:user).permit(:email, :nickname) |
| Laravel | $request->all() を Eloquent に渡す | モデルで厳格な $fillable を設定し、FormRequest の validated() のみを使用します。クライアント入力から forceFill を実行してはいけません |
| FastAPI | Pydantic の方針判断 | 書き込みでは、リクエストモデルに model_config = ConfigDict(extra="forbid") を設定します。読み取りでは ignore のままでもかまいません |
| ASP.NET Core | [FromBody] Entity | DTO を分離し、AutoMapper プロファイルを明示的に設定します。フィルタリングされていない DTO からの SetValues は避けます |
| Express/Fastify | req.body のスプレッド | 名前付きフィールドを分割代入するか、Fastify のルートスキーマで additionalProperties: false を指定し、ajv が余分な項目を取り除くようにします |
| Go | ドメイン構造体へのデコード | クライアントが書き込み可能なフィールドのみを含む専用のリクエスト構造体を使用します |
// VULNERABLE
app.useGlobalPipes(new ValidationPipe());
// FIXED
app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }));
# VULNERABLE (DRF)
class UserSerializer(ModelSerializer):
class Meta:
model = User
fields = "__all__"
# FIXED
class UserSerializer(ModelSerializer):
class Meta:
model = User
fields = ("email", "nickname")
read_only_fields = ("is_admin", "role", "user_id")
# FIXED (Rails)
def user_params
params.require(:user).permit(:email, :nickname)
end
// FIXED (Laravel)
class User extends Model {
protected $fillable = ['email', 'nickname'];
}
// controller consumes only validated data
$user = User::create($request->validated());
// FIXED (ASP.NET Core) - bind a DTO, not the entity
public record UpdateUserDto(string Nickname); // no Role, no TenantId
Pydantic の extra の判断基準:エンドポイントがクライアントによる書き込みを受け付ける場合は forbid を選択します(過剰な項目の送信を 422 にします)。ペイロードがサーバー内部でのみ使用される場合は ignore を許容します。allow は決して使用しないでください。
ゲートウェイ層での選択肢:API ゲートウェイがサービスの前段にある場合、ゲートウェイでの JSON スキーマによるリクエスト検証(ルートごとのボディスキーマで未知のプロパティを削除または拒否する方法)は、コードレベルのフィールド許可リストを導入するまでの補完策として許容されます。
レート制限とページネーション上限
プラットフォーム層とコードの両方で制限を適用します(多層防御)。幅広く使えるトークンバケットのデフォルト値は、認証エンドポイントではユーザー名と IP の組み合わせごとに毎分 5 リクエスト、一般 API ではユーザーまたは API キーごとに毎分 60~120 リクエスト(短時間のバーストを許容)、エクスポート/検索では毎分 10 リクエストです。まずユーザー ID/API キーをキーにし、次に IP をキーにします。
// Express - express-rate-limit
const { rateLimit } = require("express-rate-limit");
const loginLimiter = rateLimit({ windowMs: 60_000, max: 5, standardHeaders: true });
app.use("/api/login", loginLimiter);
// NestJS - @nestjs/throttler
@Module({ imports: [ThrottlerModule.forRoot([{ ttl: 60_000, limit: 100 }])] })
// plus @Throttle({ default: { limit: 5, ttl: 60_000 } }) on auth controllers
# Flask - slowapi / DRF settings
REST_FRAMEWORK = {
"DEFAULT_THROTTLE_CLASSES": ["rest_framework.throttling.ScopedRateThrottle"],
"DEFAULT_THROTTLE_RATES": {"login": "5/min", "search": "30/min", "export": "10/hour"},
}
# Rails - Rack::Attack
Rack::Attack.throttle("login/ip", limit: 5, period: 60) { |req| req.ip if req.path == "/api/login" }
// ASP.NET Core (.NET 7+) built-in rate limiting
builder.Services.AddRateLimiter(o => o.AddFixedWindowLimiter("auth", w => {
w.Window = TimeSpan.FromMinutes(1); w.PermitLimit = 5;
}));
クライアントからの指定にかかわらず、サーバー側でページネーションの上限を設定します。
// FIXED - clamp before querying
const limit = Math.min(parseInt(req.query.limit ?? "20", 10), 100);
const page = Math.max(parseInt(req.query.page ?? "1", 10), 1);
登録、投票、引き換えのフローには、CAPTCHA/プルーフ・オブ・ワークなどの相当する対策を追加します。API キーごとのクォータを、ゲートウェイまたはプラン階層テーブルを参照するミドルウェアで適用します。
GraphQL の堅牢化
- Apollo Server:本番環境では
introspection: falseに設定し、ランディングページプラグイン(ApolloServerPluginLandingPageGraphQLPlayground)を除外するか、ApolloServerPluginUsageReportingのような本番環境向けの制御を追加します。formatError/エラーのマスキングでフィールド候補の提案を無効にします。 - 深度/複雑度:
validationRules: [depthLimit(10)]内でgraphql-depth-limitを使用します。コスト分析(graphql-cost-analysis)または Apollo のestimateComplexityを使用し、ライブラリが対応している場合はエイリアス数にも上限を設けます。 - graphql-yoga:
plugins: [useDepthLimit({ maxDepth: 10 })]で深度制限プラグインを使用します。maskedErrorsはデフォルトで有効なので、そのまま維持します。 - Strawberry:スキーマの拡張機能リストに
strawberry.extensions.QueryDepthLimiter(max_depth=10)を追加します。 - Graphene:実行前に深度またはコストの上限を追加する検証ルール/ミドルウェアを使用します。
- 永続化クエリ:AutomaticPersistedQueries をオペレーションレジストリの許可リストに対して実行し、公開クライアントからの未登録ハッシュを拒否します。
- バッチ処理:オペレーション配列を受け付ける場合は、配列の長さと合計複雑度に上限を設けるか、公開クライアントでのバッチ処理を無効にします。
- リゾルバーの認可:リゾルバー/データローダーでオブジェクト単位のチェックを必ず実施します(AUTHZ を参照)。「スキーマは内部向け」という前提に頼ってはいけません。
// VULNERABLE
const server = new ApolloServer({ typeDefs, resolvers });
// FIXED
const server = new ApolloServer({
typeDefs, resolvers,
introspection: false,
validationRules: [depthLimit(10)],
});
gRPC の堅牢化
- 本番ビルドではリフレクションを無効にします(環境変数またはビルドタグで制御します)。リフレクションによって、攻撃者に完全なディスクリプターセットが渡ってしまいます。
- TLS はサービスまたはロードバランサーで終端します。平文の h2c ポートを外部に公開してはいけません。サーバー自体に TLS 認証情報を設定することを推奨します。
// VULNERABLE
lis, _ := net.Listen("tcp", ":50051")
s := grpc.NewServer() // plaintext
// FIXED
creds, _ := credentials.NewServerTLSFromFile("cert.pem", "key.pem")
s := grpc.NewServer(grpc.Creds(creds))
- すべての登録済みサービスを対象とするインターセプターで、RPC ごとの認証を強制します。例外は明示的かつ限定的にし(ヘルスチェックのみ)、管理します。
// FIXED - unary interceptor validating metadata
func AuthInterceptor(ctx context.Context, req interface{},
info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
md, _ := metadata.FromIncomingContext(ctx)
if len(md.Get("authorization")) == 0 { return nil, status.Error(codes.Unauthenticated, "missing token") }
return handler(ctx, req)
}
grpc.NewServer(grpc.UnaryInterceptor(AuthInterceptor), grpc.ChainStreamInterceptor(StreamAuth))
Python:interceptors=[...](例:grpc-interceptor)を指定して構築します。Java:すべてのサービスに対して ServerBuilder.addService(...) と ServerInterceptors.intercept(service, authInterceptor) を組み合わせます。
- 適切なメッセージサイズ上限(
MaxRecvMsgSize、Java のmaxInboundMessageSize)を設定し、列挙値を検証します。セキュリティ上重要なディスパッチにgoogle.protobuf.Anyを使用することは避け、保存済みメッセージを返す永続化処理の前に未知のフィールドを削除します。
OpenAPI/ドキュメントの公開ポリシー
API ドキュメントは認証の背後で公開するか、内部ネットワークに制限します。外部向けと内部向けの仕様を分け、内部エンドポイントが公開ファイルに現れないようにします。フレームワークの設定例:本番環境では springdoc.api-docs.enabled=false(またはプロファイルによる制御)を設定し、DRF の drf-spectacular/drf-yasg ビューは IsAdminUser 権限クラスで保護し、Swashbuckle の UseSwaggerUI() は if (app.Environment.IsDevelopment()) の内側に配置し、swagger-ui-express は認証ミドルウェアの後にマウントします。X-Powered-By/フレームワークのバナーを削除し(app.disable("x-powered-by")、Program.cs でのヘッダー削除)、Symfony profiler が本番環境に公開されないようにします。
WebSocket/SSE の堅牢化
アップグレード時に Origin を許可リストに照らして検証し、アクションごとにメッセージ認証を行います。ユーザーごとにトピック/ルームへの参加を認可し、イベント配信のたびに認可を再確認します。
// VULNERABLE (ws)
const wss = new WebSocket.Server({ port: 8080 }); // no origin check
// FIXED
const wss = new WebSocket.Server({
port: 8080,
verifyClient: (info) => allowlist.includes(new URL(info.origin).host),
});
wss.on("connection", (ws) => {
ws.on("message", (raw) => {
const msg = JSON.parse(raw);
if (!canAct(ws.user, msg.action, msg.targetId)) return; // per-message authz
});
});
// socket.io - origin allowlist + handshake token
new Server({ cors: { origin: ["https://app.example.com"], credentials: true } })
.use((socket, next) => next(jwt.verify(socket.handshake.auth.token) ? undefined : new Error("unauthorized")));
SignalR:AllowedOrigins("https://app.example.com") を設定します。Django Channels:ASGI アプリケーションを OriginValidator で囲み、各コンシューマーでスコープレベルの権限チェックを行います。SSE:ストリームのリクエストを認証し、Last-Event-ID による再生がリクエスト元のテナント内に収まることを検証します。
API キーの管理
キーはヘッダーのみで送信します(X-API-Key、Authorization: Bearer)。クエリ文字列のキー・パラメーターは拒否します。すべてのキーに権限セットを割り当て、リクエスト時に適用します。環境ごと、連携ごとに別々のキーを発行し、失効(リクエストごとに確認するステータスフラグ)とローテーション用のエンドポイントを実装します。キーはハッシュを保存し、平文では保存しません。モバイル/SDK ビルドに含めるのは公開クライアント識別子のみとし、秘密情報を扱う機能はバックエンド・フォー・フロントエンドの背後に配置します。
検証
GIVEN/WHEN/THEN 受け入れシナリオ
- GIVEN シリアライザーの許可リストが設定されているとき、WHEN クライアントが
{"email":"[email protected]","password":"x","is_admin":true}を POST すると、THEN サーバーは拒否されたプロパティ名を示して 400/422 を返す(拒否モード)、または余分なフィールドを除いてオブジェクトを保存する(除去モード)。その後の読み取りでis_adminが存在しないことを確認できます。 - GIVEN 正規のクライアントがあるとき、WHEN ドキュメントに記載されたフィールドだけを正確に送信すると、THEN 修正前と同様にリクエストは成功します(ネガティブテスト:過剰なブロックがないこと)。
- GIVEN レートリミッターが設定されているとき、WHEN 20 回のログイン試行が短時間に到着すると、THEN およそ 6 回目以降の試行は 429 を受け取ります。一方、通常ユーザーが間隔を空けて行う通信では 429 が発生しません。
- GIVEN ページネーションの上限が設定されているとき、WHEN
?limit=1000000が到着すると、THEN レスポンスには上限で制限されたページサイズ以下のデータが含まれ、上限が報告されます。 - GIVEN 本番環境でイントロスペクションが無効になっているとき、WHEN
{"query":"{__schema{types{name}}}"}が送信されると、THEN エンドポイントはエラーを返し、型名を一切漏らしません。WHEN 登録済みクライアントが永続化クエリのハッシュを送信すると、THEN 実行は引き続き成功します。 - GIVEN 深さの上限が設定されているとき、WHEN 上限を超える深さのネストされた選択を実行すると、THEN リゾルバーが実行される前に、深さルールを示す検証エラーが発生します。
- GIVEN gRPC 認証インターセプターがインストールされているとき、WHEN authorization メタデータなしで RPC が呼び出されると、THEN ストリームを含むすべてのサービスで、呼び出しは
Unauthenticatedで失敗します。 - GIVEN ドキュメントへのアクセス制限が設定されているとき、WHEN 未認証のクライアントが
/openapi.jsonまたは/swagger-ui.htmlを取得すると、THEN 401/403 またはリダイレクトが返されます。 - GIVEN WebSocket の Origin 検証が設定されているとき、WHEN ハンドシェイクで
Origin: https://evil.exampleが提示されると、THEN サーバーはアップグレードを拒否します(101 を返しません)。 - GIVEN メッセージごとの認可が設定されているとき、WHEN 接続済みのソケットが別のユーザーのリソース ID に対するアクションを要求すると、THEN そのアクションは拒否されます。
回帰契約テストの擬似コード
# CI contract test: unknown fields must never reach persistence
def test_register_rejects_unknown_fields(api_client):
payload = {"email": "[email protected]", "password": "Str0ng!passphrase", "is_admin": True}
r = api_client.post("/api/register", json=payload)
assert r.status_code in (400, 422) or "is_admin" not in r.json()
login = api_client.post("/api/login", json={"email": "[email protected]", "password": "Str0ng!passphrase"})
me = api_client.get("/api/me", headers=auth(login))
assert me.json().get("is_admin") is not True
def test_login_throttled():
codes = [post_login(wrong_password=True).status_code for _ in range(20)]
assert 429 in codes
def test_list_endpoint_capped():
body = get_json("/api/users?limit=1000000")
assert len(body["items"]) <= 100
手動チェックリスト
- すべてのルートグループに認証 AND スロットルのミドルウェアが適用されていること(グループ間でミドルウェア一覧を比較します)。
- すべての書き込みエンドポイントで、明示的なフィールドリストを持つ DTO/シリアライザーをバインドしていること。
- すべてのリストエンドポイントで、ページサイズをサーバー側で制限していること。
- GraphQL の初期化で、イントロスペクションを無効にし、ランディングページを無効にし、深さと複雑度の上限を有効にし、候補をマスクしていること。
- 本番環境のパスでは、gRPC サーバーがリフレクションを一切登録せず、TLS を使用するか TLS 終端の背後に配置され、すべてのサービスで認証インターセプターを連結していること。
- 状態を変更する GET ルートがなく、オーバーライドミドルウェアが制限され、CSRF に対して安全であること。
- ドキュメントのエンドポイントが制限され、仕様ファイルが静的アセットや公開バケットに存在しないこと。
- キーがヘッダー経由でのみ受け付けられ、スコープチェックがリクエスト時に行われ、失効手段が存在すること。
- WebSocket のアップグレードで Origin を検証し、メッセージハンドラーでアクションを認可し、ルームへの参加時に ACL をチェックしていること。
修正後に再実行する Grep
rg -n 'fields\s*=\s*["'"'"']__all__["'"'"']' --type py
rg -n '\$request->all\(\)'
rg -n 'ValidationPipe\(\)'
rg -n 'introspection\s*:\s*true|GRAPHQL_INTROSPECTION'
rg -n 'reflection\.Register|enable_server_reflection|ProtoReflectionService'
rg -n 'insecure\.NewCredentials|WithTransportCredentials\(insecure|h2c'
rg -n '_method|X-HTTP-Method-Override'
rg -n '[\?&]api[_-]?key='
修正後に想定される状態:各クエリは、レビューされ承認された出現箇所(開発環境で制限されたもの、許可リストに登録されたもの、または文書化された例外)のみを返します。
重大度評価
評価基準
| 脆弱性 | 基準となる深刻度 | CVSS v3.1 ベクター | CWE |
|---|---|---|---|
is_admin/role への一括代入(権限昇格) | Critical | CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H | CWE-915 |
セキュリティに関係しないフィールド(price、user_id)への一括代入 | High | CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:H/A:L | CWE-915 |
| スロットリングがないことのみ(ロックアウト回避の実証なし) | Low-Medium、状況による | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:L | CWE-770, CWE-799 |
| ページネーションがない/機微なテーブルを一度にダンプ可能 | データの種類により Medium-High | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N | CWE-770 |
| イントロスペクション+プレイグラウンド+ドキュメントの公開 | 情報漏えいとして Low-Medium | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N | CWE-200 |
| フィールド候補の漏えい/スタックトレースを含むエラー応答 | Low-Medium | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N | CWE-200 |
| API の認証情報付き CORS ワイルドカード | セッション情報を伴う場合は High | CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:L/A:N | CWE-942 |
| URL での API キーの送信 | High | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N | CWE-798 |
| アカウント全体へのアクセスを許可する、埋め込み SDK の共有キー | High | 配布バイナリからの到達可能性を評価 | CWE-798 |
| WebSocket の乗っ取り(オリジンチェックなし、メッセージごとの認証なし) | High | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:L | CWE-346, CWE-862 |
| ビジネス RPC における gRPC の平文通信+RPC ごとの認証なし | 外部から到達可能な場合は High | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N | CWE-306 |
| 現行の修正が適用されていない非推奨 API バージョンが稼働中 | インベントリーリスクとして Medium | CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:N | CWE-200 |
重大度の調整
脆弱な対象がインターネットから到達可能で、かつ認証不要の場合は、深刻度を 1 段階引き上げます。gRPC ポートなど、内部ネットワーク上の位置が必要な場合、同等の権限をすでに持つ認証済みの低権限コンテキストが悪用に必要な場合、または代替となるプラットフォーム制御(ゲートウェイのレート制限、WAF)が問題を確実にカバーしている場合は、1 段階引き下げます。いずれの場合も、指摘には制御を定義しているファイルを記載してください。レート制限に関する指摘は、クレデンシャルスタッフィング、列挙、または金銭の流れと組み合わさって High になる場合を除き、Low-Medium にとどめます。
よくある誤検出
- 管理者専用または読み取り専用の ViewSet での DRF
fields = "__all__":検出対象として報告する前に、ルートの権限クラスを確認してください。IsAdminUserを使用する内部専用シリアライザーは許容されます。 - ハンドラーが永続化の前に名前付きフィールドを分割代入する場合の、
whitelistを設定していない NestJS DTO:バインディングは寛容ですが、データの送信先は安全です。パイプだけでなく、送信先を追跡してください。 - ゲートウェイ側でスロットリングを処理している場合:nginx の
limit_req、Envoy のレート制限フィルター、Cloudflare のルールがあれば、この制御を満たしています。インフラ設定を確認し、制御の欠如を報告するのではなく、補完的な制御として記録してください。 - 実際に公開読み取り可能な API で意図的にイントロスペクションを公開している場合:検出対象として報告する前に、変更可能な機能や機密性の高い型が存在しないことを確認してください。情報提供として報告してください。
- Go/JSON の厳格性:Go のデコーダーは未知の JSON キーを黙って破棄するため、安全なエクスポートフィールドのみを持つ構造体へのデコードは、リクエストボディ全体をバインドしていてもマスアサインメントには当たりません。構造体が機密フィールド(
IsAdmin、Role)をエクスポートしている場合に限り、検出対象として報告してください。 - Pydantic のデフォルト
extra=ignore:未知のフィールドは破棄されるため、extra="allow"が設定されているか、生の辞書が送信先に渡される場合を除き、FastAPI エンドポイントで過剰なフィールドを送信できることはほとんどありません。 - Rails/Laravel の
_method/オーバーライドミドルウェア:組み込み実装では、安全な動詞に限定され、CSRF 保護と統合されています。フレームワーク機能そのものではなく、カスタムまたは制限のないオーバーライド処理を検出対象として報告してください。 - 資格情報を使用しないワイルドカード CORS:ブラウザーはワイルドカードと資格情報の組み合わせを拒否します。実際の不具合は、資格情報を使用してオリジンを反映することです(対応表については CONFIG を相互参照してください)。
- クエリパラメーターがなくても、カーソルのデフォルト設定や ORM の
LIMITのデフォルト設定によってデータベース層でページネーションが行われている場合:実際のクエリ構築を確認してください。 - 本番ビルドでは確実に除外されるビルドタグ/環境変数のゲートの背後で gRPC リフレクションが有効になっている場合:制御の証拠としてゲートを記載してください。
/v1ルートが同一のハンドラー群と現行のミドルウェアを経由する場合のバージョンの乱立:ハンドラーが共有されていればパッチの欠落はありません。コードパスが異なる場合に限り報告してください。- 明示的なデバッグフラグが有効な場合にのみエラースタックトレースが表示され、そのフラグが本番設定ではデフォルトで無効になっている場合:デプロイ済みの設定値を確認してください。
参考資料
OWASP API Security Top 10 (2023) の対応表
プロジェクトホーム:https://owasp.org/www-project-api-security/
| OWASP 項目 | このモジュールの対象範囲 |
|---|---|
| API1:2023 Broken Object Level Authorization | GraphQL リゾルバーのオブジェクトレベルの欠落、WebSocket のトピック/アクション、IDOR が可能な送信先(主な対象は AUTHZ モジュール) |
| API2:2023 Broken Authentication | 認証エンドポイントに対するプラットフォームのスロットリング、キーの転送/スコープ設定(ロックアウトの詳細は AUTHN モジュール) |
| API3:2023 Broken Object Property Level Authorization | マスアサインメント/過剰なフィールド送信の一連のクラス |
| API4:2023 Unrestricted Resource Consumption | レート制限、ページネーション上限、深度/複雑度制限、メッセージサイズ上限 |
| API5:2023 Broken Function Level Authorization | ルートグループ間のミドルウェアの整合性、ドキュメント/イントロスペクションを介した非公開エンドポイントの露出 |
| API6:2023 Unrestricted Access to Sensitive Business Flows | サインアップ/引き換え/投票フローにおける自動化対策 |
| API7:2023 Server Side Request Forgery | ここでは対象外 — DESER/SSRF モジュールで扱います |
| API8:2023 Security Misconfiguration | イントロスペクション/プレイグラウンド/ドキュメント/デバッグヘッダー/CORS/コンテンツタイプの混同/gRPC リフレクション |
| API9:2023 Improper Inventory Management | バージョンの乱立、古い仕様書、廃止予定バージョンの稼働 |
| API10:2023 Unsafe Consumption of APIs | 一部を対象(サードパーティに対するキーのスコープ設定)。全範囲の対象は先送りされています。 |
CWE エントリー
- CWE-915 動的に決定されるオブジェクト属性の不適切な制御による変更: https://cwe.mitre.org/data/definitions/915.html
- CWE-770 制限やスロットリングを設けずにリソースを割り当てる: https://cwe.mitre.org/data/definitions/770.html
- CWE-799 インタラクション頻度の不適切な制御: https://cwe.mitre.org/data/definitions/799.html
- CWE-306 重要な機能に対する認証の欠如: https://cwe.mitre.org/data/definitions/306.html
- CWE-862 認可の欠如: https://cwe.mitre.org/data/definitions/862.html
- CWE-639 ユーザーが制御するキーを介した認可のバイパス: https://cwe.mitre.org/data/definitions/639.html
- CWE-200 認可されていない主体への機密情報の漏えい: https://cwe.mitre.org/data/definitions/200.html
- CWE-400 リソース消費の制御不能: https://cwe.mitre.org/data/definitions/400.html
- CWE-798 ハードコードされた認証情報の使用: https://cwe.mitre.org/data/definitions/798.html
- CWE-346 オリジン検証エラー: https://cwe.mitre.org/data/definitions/346.html
- CWE-942 信頼されていないドメインを含む、過度に許可的なクロスドメインポリシー: https://cwe.mitre.org/data/definitions/942.html
参考資料
- OWASP チートシートシリーズ — GraphQL: https://cheatsheetseries.owasp.org/cheatsheets/GraphQL_Cheat_Sheet.html
- OWASP チートシートシリーズ — REST セキュリティ: https://cheatsheetseries.owasp.org/cheatsheets/REST_Security_Cheat_Sheet.html
- OWASP チートシートシリーズ — サービス拒否: https://cheatsheetseries.owasp.org/cheatsheets/Denial_of_Service_Cheat_Sheet.html
- OWASP チートシートシリーズ — HTML5(WebSocket のオリジンに関するガイダンス): https://cheatsheetseries.owasp.org/cheatsheets/HTML5_Security_Cheat_Sheet.html
- RFC 6455 第 10 節(WebSocket のオリジンに関する考慮事項): https://datatracker.ietf.org/doc/html/rfc6455#section-10
- GraphQL 仕様のセキュリティに関する注記: https://graphql.org/learn/security/
- socket.io の CORS 処理: https://socket.io/docs/v4/handling-cors/
- gRPC 認証ガイド: https://grpc.io/docs/guides/auth/