APIに潜む見えない脅威
OAuth2フローの強化やJWTキーのローテーションに40時間のスプリントを丸々費やしたとしましょう。あなたは安全だと感じているかもしれません。しかし、多くのAPIにとって、正面玄関は施錠されていても、裏の窓が全開になっていることがあります。最大の脅威は、ハッカーによるシステム侵入ではなく、正当なユーザーが自分のものではないデータにアクセスすることである場合が多いのです。これは「オブジェクトレベルの認可の不備(BOLA:Broken Object Level Authorization)」と呼ばれ、長年にわたってOWASP API Security Top 10の第1位に君臨しています。
セキュリティは、開発の最後に付け加える「あれば良い」機能ではありません。午前3時にボットネットが私のサーバーにSSH総当たり攻撃を仕掛けてくるのを目の当たりにして以来、私はセキュリティを「フェーズ2」の後回しタスクとして扱うのをやめました。その経験から、ハッカーは常に最も抵抗の少ない道を探していることが証明されました。現代のアプリケーションにおいて、その道とは通常、投げ込まれたuser_idやorder_idを鵜呑みにしてしまう、保護の不十分なAPIエンドポイントのことです。
5分以内にZAPを起動する
OWASP ZAP(Zed Attack Proxy)は、これらの穴を見つけるための業界標準のユーティリティです。HTMLリンクをクロールするだけの古いウェブスキャナーとは異なり、ZAPはAPI定義を読み取ってアプリケーションの隠れた隅々までマッピングします。
1. インストーラーを入手する
お使いのOSに合わせてZAPをダウンロードしてください。起動したら、マーケットプレイス(カラフルな箱のアイコン)をチェックして、OpenAPI Supportアドオンが有効になっていることを確認します。現代のAPIと通信するにはこれが必要です。
2. API定義で効率化する
APIにSwaggerドキュメント(通常は/swagger.jsonや/api-docsにあります)がある場合、それをインポートするのが開始への最短ルートです。手動で探索するよりも常に効率的です。
- Import > Import an OpenAPI definition from a URL に移動します。
- ドキュメントのURLを貼り付けます。
- ZAPが「Sites」ツリーにすべてのエンドポイントを表示するのを確認します。
3. ベースラインスキャンから始める
「Sites」タブでAPIホストを右クリックし、Attack > Active Scan を選択します。この包括的なスキャンは、SQLインジェクションやXSSを探します。しかし、BOLAはデータの所有権の理解が必要なため、通常はこのスキャンでは見逃されます。より深く掘り下げる必要があります。
BOLA攻撃の構造
BOLAは、サーバーがセッションの所有者をチェックせずにURL内のIDを信頼したときに発生します。これは、ホテルのキーカードがフロアのすべてのドアを開けてしまうのと同じデジタル版の脆弱性です。
請求書の詳細を取得するエンドポイントを考えてみましょう:
GET /api/v1/invoices/9901
ユーザーAとしてログインしている場合、請求書9901が表示されるべきです。しかし、その番号を9902に変更したらどうなるでしょうか?サーバーがログインしているかどうか(認証)だけを確認し、請求書9902が本当に自分のものかどうか(認可)を確認し忘れている場合、データ漏洩が発生していることになります。
従来のスキャナーが失敗する理由
自動化ツールはパターンを好みます。500エラーやデータベースの漏洩は即座に認識します。しかし、BOLAのエクスプロイトは完全に有効なリクエストに見えます。サーバーは有効なJSONと共に200 OKを返します。単純なツールにとって、それは成功に見えます。しかしビジネスにとっては、機密性の高い顧客レコードの漏洩を意味します。
ZAPでBOLAのファジングを行う
BOLAを見つけるために、IDを体系的に推測する攻撃者を模倣します。ZAPのFuzzer(ファザー)はこの作業に最適なツールです。
ステップ1:リクエストをキャプチャする
ZAPの「History」タブから、/api/users/me/profileや/api/orders/101のようにIDを含むリクエストを見つけます。
ステップ2:ファジングを設定する
- リクエストを右クリック > Fuzz… を選択します。
- 数値ID(例:
101)をハイライトし、Add… をクリックします。 - Payloadsウィンドウで Series を選択し、100から1,000までの範囲を生成します。
- Start Fuzzer をクリックします。
ステップ3:異常を特定する
Fuzzerタブの Size Resp. Body(レスポンスボディサイズ) カラムに注目してください。リクエストの99%が400バイトの「Forbidden(閲覧禁止)」メッセージを返しているのに、1つだけ12KBのデータを返している場合、それが「決定的な証拠」です。その12KBのレスポンスは、おそらく他のユーザーに属するデータです。
プロのヒント:アクセス制御テスト
プロフェッショナルな監査には、Access Control Testing アドオンを使用してください。2人のユーザー(ユーザーAとユーザーB)を定義し、ユーザーAとしてセッションを記録した後、ユーザーBのトークンを使用してそれを再実行するようにZAPに指示できます。もしユーザーBがユーザーAのプライベートデータの取得に成功した場合、ツールが自動的にフラグを立てます。
# cURLを使用した手動BOLAテスト
# ユーザーBのレコードを取得しようとするユーザーAのトークン
curl -H "Authorization: Bearer <ユーザーAのトークン>" \
"https://api.example.com/v1/private-data/user-B-id"
根本原因の修正
脆弱性を見つけることは大きな勝利ですが、本当の仕事はそれを完全に塞ぐことです。これには、データアクセスに対する考え方を変える必要があります。
1. クライアント側で指定されたIDの使用をやめる
ユーザーが自分のプロフィールを求めている場合、/api/users/123に頼ってはいけません。/api/users/meを使用してください。サーバーは安全なセッションまたはJWTから直接ユーザーIDを抽出するべきであり、これによりユーザーが別のIDを推測することは不可能になります。
2. すべてのクエリで所有権を強制する
すべてのデータベースクエリには所有権のチェックを含める必要があります。IDだけで取得するのではなく、IDと所有者で取得してください。
# 安全な例
def get_order(order_id, current_user_id):
# このクエリは、注文がリクエストした本人に属していることを確認します
return db.query("SELECT * FROM orders WHERE id = ? AND user_id = ?",
order_id, current_user_id)
3. UUIDに切り替える
連番ID(1, 2, 3…)は、攻撃者にデータベース全体をスクレイピングするよう誘っているようなものです。550e8400-e29b-41d4-a716-446655440000のようなUUIDを使用してもロジックのバグは直りませんが、IDを「推測」することは事実上不可能になります。
4. パイプラインでセキュリティを自動化する
年1回の手動監査を待ってはいけません。GitHub Actionsの一部として、ヘッドレスなDockerコンテナでOWASP ZAPを実行しましょう。これにより、1行のコードでも本番環境に触れる前に、すべてのプルリクエストに対して基本的なAPIの欠陥がチェックされるようになります。
# CI/CDパイプラインでのZAP APIスキャンの実行
docker run -v $(pwd):/zap/wrk/:rw -t owasp/zap2docker-stable zap-api-scan.py \
-t https://api.example.com/swagger.json -f openapi -r report.html
安全なAPIの構築は反復的なプロセスです。ZAPの自動化と「すべてを検証する」というマインドセットを組み合わせることで、野生に潜む最も危険なAPIエクスプロイトからユーザーのデータを守ることができます。

