mcpField
E
exploiting-excessive-data-exposure-in-api

exploiting-excessive-data-exposure-in-api

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

exploiting-excessive-data-exposure-in-api について

書面による許可を得た API セキュリティテスト向けのガイドです。REST、モバイル、GraphQL API の余分な応答フィールドや機密データを調べ、応答比較から報告までの手順を示します。

主な機能

  1. 01仕様と実際の応答の比較
  2. 02機密データパターンの検出
  3. 03UI と応答フィールドの比較
  4. 04ネスト構造と GraphQL フィールドの調査
  5. 05ヘッダーとエラー情報の漏えい確認
  6. 06セキュリティ所見の報告書作成

活用シーン

  1. 01モバイル API のデータ露出評価
  2. 02応答内の個人情報や決済情報の発見
  3. 03GraphQL の機密フィールド確認
  4. 04エラー応答に含まれる内部情報の調査

インストール

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

$npx skills add https://github.com/mukul975/anthropic-cybersecurity-skills --skill exploiting-excessive-data-exposure-in-api

インストール後、Claude Code で/exploiting-excessive-data-exposure-in-apiを入力して読み込まれたか確認します。

同梱ファイル

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

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

詳細

API における過剰なデータ露出の悪用

使用する状況

  • フロントエンドにはデータの一部だけが表示される一方、API レスポンスには追加のフィールドが含まれている API をテストする場合
  • 複数のクライアント種別向けに設計されたレスポンスに過剰なデータが含まれている可能性のある、モバイルアプリケーションの API を評価する場合
  • UI には表示されないメールアドレス、電話番号、SSN、決済データなどが API レスポンスに含まれることによる、PII の漏えいを特定する場合
  • クライアントが機密属性を含む任意のフィールドをリクエストできる GraphQL API をテストする場合
  • マイクロサービスのリファクタリング後、内部サービス間のデータが公開エンドポイントに漏れていないか API を評価する場合

書面による許可なしに使用しないでください。 データ露出のテストでは、機密性の高い個人データを取得して分析する場合があります。

前提条件

  • 対象となる API エンドポイントと範囲を明記した書面による許可
  • インターセプトプロキシとして設定済みの Burp Suite Professional または mitmproxy
  • 異なる権限レベルのテストアカウントを 2 つ(一般ユーザーと管理者)
  • トラフィックを取得するためのブラウザー開発者ツールまたはモバイルプロキシの設定
  • requests および json ライブラリを含む Python 3.10+
  • 実際のレスポンスと比較するための API ドキュメント(OpenAPI 仕様)

法的通知: このスキルは、許可を得たセキュリティテストおよび教育目的に限って使用してください。自分が所有していない、またはテストの書面による許可を得ていないシステムに対する不正な使用は違法であり、コンピューター詐欺に関する法律に違反する可能性があります。

ワークフロー

ステップ 1:レスポンススキーマの特定

ドキュメントに記載された API レスポンスと実際のレスポンスを比較します。

import requests
import json

BASE_URL = "https://target-api.example.com/api/v1"
headers = {"Authorization": "Bearer <user_token>", "Content-Type": "application/json"}

# Fetch a resource and analyze all returned fields
endpoints_to_test = [
    ("GET", "/users/me", None),
    ("GET", "/users/me/orders", None),
    ("GET", "/products", None),
    ("GET", "/users/me/settings", None),
    ("GET", "/transactions", None),
]

for method, path, body in endpoints_to_test:
    resp = requests.request(method, f"{BASE_URL}{path}", headers=headers, json=body)
    if resp.status_code == 200:
        data = resp.json()
        # Recursively extract all field names
        def extract_fields(obj, prefix=""):
            fields = []
            if isinstance(obj, dict):
                for k, v in obj.items():
                    full_key = f"{prefix}.{k}" if prefix else k
                    fields.append(full_key)
                    fields.extend(extract_fields(v, full_key))
            elif isinstance(obj, list) and obj:
                fields.extend(extract_fields(obj[0], f"{prefix}[]"))
            return fields

        all_fields = extract_fields(data)
        print(f"\n{method} {path} - {len(all_fields)} fields returned:")
        for f in sorted(all_fields):
            print(f"  {f}")
ステップ 2:機密データのパターン検出

API レスポンスをスキャンして、機密データのパターンを検出します。

import re

SENSITIVE_PATTERNS = {
    "email": r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}',
    "phone": r'(\+?1?\s?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4})',
    "ssn": r'\b\d{3}-\d{2}-\d{4}\b',
    "credit_card": r'\b(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13})\b',
    "password_hash": r'\$2[aby]?\$\d{2}\$[./A-Za-z0-9]{53}',
    "api_key": r'(?:api[_-]?key|apikey)["\s:=]+["\']?([a-zA-Z0-9_\-]{20,})',
    "internal_ip": r'\b(?:10\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])|192\.168)\.\d{1,3}\.\d{1,3}\b',
    "aws_key": r'AKIA[0-9A-Z]{16}',
    "jwt_token": r'eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+',
    "uuid": r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}',
}

SENSITIVE_FIELD_NAMES = [
    "password", "password_hash", "secret", "token", "ssn", "social_security",
    "credit_card", "card_number", "cvv", "pin", "private_key", "api_key",
    "internal_id", "debug", "trace", "stack_trace", "created_by_ip",
    "last_login_ip", "salt", "session_id", "refresh_token", "mfa_secret",
    "date_of_birth", "bank_account", "routing_number", "tax_id"
]

def scan_response(endpoint, response_text):
    findings = []
    # Check for sensitive data patterns in values
    for pattern_name, pattern in SENSITIVE_PATTERNS.items():
        matches = re.findall(pattern, response_text)
        if matches:
            findings.append({
                "endpoint": endpoint,
                "type": "sensitive_value",
                "pattern": pattern_name,
                "count": len(matches),
                "sample": matches[0][:20] + "..." if len(matches[0]) > 20 else matches[0]
            })

    # Check for sensitive field names
    response_lower = response_text.lower()
    for field in SENSITIVE_FIELD_NAMES:
        if f'"{field}"' in response_lower or f"'{field}'" in response_lower:
            findings.append({
                "endpoint": endpoint,
                "type": "sensitive_field",
                "field_name": field
            })

    return findings

# Scan all endpoint responses
for method, path, body in endpoints_to_test:
    resp = requests.request(method, f"{BASE_URL}{path}", headers=headers, json=body)
    if resp.status_code == 200:
        findings = scan_response(f"{method} {path}", resp.text)
        for f in findings:
            print(f"[FINDING] {f['endpoint']}: {f['type']} - {f.get('pattern', f.get('field_name'))}")
ステップ 3:UI の表示内容と API レスポンスの比較
# Fields the UI shows (observed from the frontend application)
ui_displayed_fields = {
    "/users/me": {"name", "email", "avatar_url", "role"},
    "/users/me/orders": {"order_id", "date", "status", "total"},
    "/products": {"id", "name", "price", "image_url", "description"},
}

# Fields the API actually returns
for method, path, body in endpoints_to_test:
    resp = requests.request(method, f"{BASE_URL}{path}", headers=headers, json=body)
    if resp.status_code == 200:
        data = resp.json()
        if isinstance(data, list):
            actual_fields = set(data[0].keys()) if data else set()
        elif isinstance(data, dict):
            # Handle paginated responses
            items_key = next((k for k in data if isinstance(data[k], list)), None)
            if items_key and data[items_key]:
                actual_fields = set(data[items_key][0].keys())
            else:
                actual_fields = set(data.keys())
        else:
            continue

        expected = ui_displayed_fields.get(path, set())
        excess = actual_fields - expected
        if excess:
            print(f"\n{method} {path} - EXCESS FIELDS (not shown in UI):")
            for field in sorted(excess):
                print(f"  - {field}")
ステップ 4:関連するエンドポイントでのユーザーオブジェクトの露出をテスト
# Many APIs embed full user objects in responses for orders, comments, etc.
endpoints_with_user_objects = [
    "/orders",          # Each order may include full seller/buyer profile
    "/comments",        # Comments may include full author profile
    "/reviews",         # Reviews may expose reviewer details
    "/transactions",    # Transactions may include counterparty info
    "/team/members",    # Team listing may expose excessive member data
]

for path in endpoints_with_user_objects:
    resp = requests.get(f"{BASE_URL}{path}", headers=headers)
    if resp.status_code == 200:
        text = resp.text
        # Check for user data leakage in nested objects
        user_fields_found = []
        for field in ["password_hash", "last_login_ip", "mfa_enabled", "phone_number",
                      "date_of_birth", "ssn", "internal_notes", "salary", "address"]:
            if f'"{field}"' in text:
                user_fields_found.append(field)
        if user_fields_found:
            print(f"[EXCESSIVE] {path} exposes user fields: {user_fields_found}")
ステップ 5:GraphQL の過剰なデータ取得の分析
# GraphQL allows clients to request any available field
GRAPHQL_URL = f"{BASE_URL}/graphql"

# Introspection query to discover all fields on User type
introspection = {
    "query": """
    {
      __type(name: "User") {
        fields {
          name
          type {
            name
            kind
          }
        }
      }
    }
    """
}

resp = requests.post(GRAPHQL_URL, headers=headers, json=introspection)
if resp.status_code == 200:
    fields = resp.json().get("data", {}).get("__type", {}).get("fields", [])
    print("Available User fields via GraphQL:")
    for f in fields:
        sensitivity = "SENSITIVE" if f["name"] in SENSITIVE_FIELD_NAMES else "normal"
        print(f"  {f['name']} ({f['type']['name']}) [{sensitivity}]")

# Try to query sensitive fields
sensitive_query = {
    "query": """
    query {
      users {
        id
        email
        passwordHash
        socialSecurityNumber
        internalNotes
        lastLoginIp
        mfaSecret
        apiKey
      }
    }
    """
}
resp = requests.post(GRAPHQL_URL, headers=headers, json=sensitive_query)
if resp.status_code == 200 and "errors" not in resp.json():
    print("[CRITICAL] GraphQL exposes sensitive user fields without restriction")
ステップ 6:デバッグデータと内部データの漏えい
# Test for debug information in responses
debug_headers_to_check = [
    "X-Debug-Token", "X-Debug-Info", "Server", "X-Powered-By",
    "X-Request-Id", "X-Correlation-Id", "X-Backend-Server",
    "X-Runtime", "X-Version", "X-Build-Version"
]

resp = requests.get(f"{BASE_URL}/users/me", headers=headers)
for h in debug_headers_to_check:
    if h.lower() in {k.lower(): v for k, v in resp.headers.items()}:
        print(f"[INFO LEAK] Header {h}: {resp.headers.get(h)}")

# Test error responses for stack traces
error_payloads = [
    ("GET", "/users/invalid-id-format", None),
    ("POST", "/orders", {"invalid": "payload"}),
    ("GET", "/users/-1", None),
    ("GET", "/users/0", None),
]

for method, path, body in error_payloads:
    resp = requests.request(method, f"{BASE_URL}{path}", headers=headers, json=body)
    if resp.status_code >= 400:
        text = resp.text.lower()
        if any(kw in text for kw in ["stack trace", "traceback", "at com.", "at org.",
                                      "file \"", "line ", "exception", "sql", "query"]):
            print(f"[DEBUG LEAK] {method} {path} -> {resp.status_code}: Contains stack trace or query info")

主な概念

用語定義
過剰なデータ露出API がクライアントに必要な数を超えるデータフィールドを返し、機密情報をユーザーに見せないためにフロントエンドのフィルタリングに依存している状態
過剰なデータ取得特定の操作に必要な量を超えるデータをリクエストまたは受信すること。固定のレスポンススキーマを返す REST API でよく見られます
レスポンスのフィルタリングAPI レスポンスデータをクライアント側でフィルタリングし、関連するフィールドだけを表示すること。レスポンス全体をインターセプトできるため、セキュリティ上の効果はありません
オブジェクトプロパティレベルの認可OWASP API3:2023。ユーザーがアクセスを許可されたオブジェクトプロパティのみを読み書きできるようにすること
PII の漏えい名前、メールアドレス、住所、SSN、金融データなど、個人を特定できる情報が API レスポンスで意図せず露出すること
スキーマ検証API レスポンスが定義済みスキーマに準拠するように強制し、送信前に許可されていないフィールドを除去すること

ツールとシステム

  • Burp Suite Professional:API レスポンスをインターセプトし、Comparer ツールを使用して想定されるレスポンススキーマと実際のスキーマの差分を確認します
  • mitmproxy:Python ベースのコンテンツ検査スクリプトでレスポンスを自動分析できる、スクリプト可能なプロキシ
  • OWASP ZAP:ヘッダー、エラーメッセージ、レスポンスボディに含まれる情報漏えいをパッシブスキャンで検出します
  • Postman:テストスクリプトを使用して、ドキュメントに記載されたレスポンススキーマと実際の API レスポンスを比較します
  • jq:API レスポンスから特定のフィールドを抽出して分析するためのコマンドライン JSON プロセッサ

よくあるシナリオ

シナリオ:モバイルバンキング API のデータ漏えい評価

コンテキスト:モバイルバンキングアプリの API は、アカウントのニックネームと残高のみを表示するモバイルクライアントに、アカウントオブジェクト全体を返します。API は iOS アプリ、Android アプリ、ウェブポータルから利用されます。

アプローチ:

  1. テスト端末で mitmproxy を設定し、テストユーザーとして認証します
  2. ユーザーのセッション全体(ログイン、アカウントの表示、送金、ログアウト)中のすべての API レスポンスをキャプチャします
  3. GET /api/v1/accounts のレスポンスを分析します。UI には 4 つのフィールドが表示されますが、API は 23 個のフィールドを返します
  4. API が routing_number、account_holder_ssn_last4、internal_risk_score、kyc_verification_status、linked_external_accounts を返すことを確認します。これらは UI には表示されません
  5. GET /api/v1/transactions のレスポンスを分析します。API はクライアントに不要な merchant_id、terminal_id、authorization_code、processor_response の各フィールドを返します
  6. GET /api/v1/users/me を確認します。API は last_login_ip、mfa_backup_codes_remaining、account_officer_name、credit_score_band を返します
  7. エラーレスポンスをテストします。無効なペイロードで POST /api/v1/transfers を実行すると、エラーメッセージに SQL テーブル名が含まれます

落とし穴:

  • トップレベルのフィールドだけを確認し、深くネストされたオブジェクト内の機密データを見落とす
  • ページネーションされたレスポンスをテストせず、後続のページに異なるフィールドが含まれる可能性を見落とす
  • サーバーのバージョン、バックエンド技術、内部ルーティング情報が漏れる可能性のあるレスポンスヘッダーを無視する
  • エラーレスポンスに含まれるデータ漏えいを見落とす。エラーレスポンスには、スタックトレース、SQL クエリ、内部パスが含まれることがよくあります
  • HTTPS 暗号化によってデータ漏えいを防げると思い込む(HTTPS が保護するのは通信中のデータであり、認証済みのクライアントからの漏えいではありません)

出力形式

## Finding: Excessive Data Exposure in Account and Transaction APIs

**ID**: API-DATA-001
**Severity**: High (CVSS 7.1)
**OWASP API**: API3:2023 - Broken Object Property Level Authorization
**Affected Endpoints**:
  - GET /api/v1/accounts
  - GET /api/v1/transactions
  - GET /api/v1/users/me

**Description**:
The API returns full database objects to the client, including sensitive fields
that are not displayed in the mobile application UI. The mobile app filters
these fields client-side, but they are fully accessible by intercepting the
API response. This exposes SSN fragments, internal risk scores, and KYC
verification data for any authenticated user.

**Excess Fields Discovered**:
- /accounts: routing_number, account_holder_ssn_last4, internal_risk_score,
  kyc_verification_status, linked_external_accounts (18 excess fields total)
- /transactions: merchant_id, terminal_id, authorization_code,
  processor_response (12 excess fields total)
- /users/me: last_login_ip, mfa_backup_codes_remaining, credit_score_band

**Impact**:
An authenticated user can extract sensitive financial data, internal risk
assessments, and PII for their own account that the application is not
intended to reveal. Combined with BOLA vulnerabilities, this data could
be extracted for all users.

**Remediation**:
1. Implement server-side response filtering using DTOs/view models that only include fields needed by the client
2. Use GraphQL field-level authorization or REST response schemas per endpoint per role
3. Remove sensitive fields from API responses at the serialization layer
4. Implement response schema validation in the API gateway to strip undocumented fields
5. Add automated tests that verify response schemas match documentation

コメント