exploiting-excessive-data-exposure-in-api について
書面による許可を得た API セキュリティテスト向けのガイドです。REST、モバイル、GraphQL API の余分な応答フィールドや機密データを調べ、応答比較から報告までの手順を示します。
主な機能
- 01仕様と実際の応答の比較
- 02機密データパターンの検出
- 03UI と応答フィールドの比較
- 04ネスト構造と GraphQL フィールドの調査
- 05ヘッダーとエラー情報の漏えい確認
- 06セキュリティ所見の報告書作成
活用シーン
- 01モバイル API のデータ露出評価
- 02応答内の個人情報や決済情報の発見
- 03GraphQL の機密フィールド確認
- 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 と一緒にインストールされる参考資料やスクリプト。
LICENSEreferences/api-reference.mdscripts/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 アプリ、ウェブポータルから利用されます。
アプローチ:
- テスト端末で mitmproxy を設定し、テストユーザーとして認証します
- ユーザーのセッション全体(ログイン、アカウントの表示、送金、ログアウト)中のすべての API レスポンスをキャプチャします
GET /api/v1/accountsのレスポンスを分析します。UI には 4 つのフィールドが表示されますが、API は 23 個のフィールドを返します- API が
routing_number、account_holder_ssn_last4、internal_risk_score、kyc_verification_status、linked_external_accountsを返すことを確認します。これらは UI には表示されません GET /api/v1/transactionsのレスポンスを分析します。API はクライアントに不要なmerchant_id、terminal_id、authorization_code、processor_responseの各フィールドを返しますGET /api/v1/users/meを確認します。API はlast_login_ip、mfa_backup_codes_remaining、account_officer_name、credit_score_bandを返します- エラーレスポンスをテストします。無効なペイロードで
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
コメント
Markdown 対応