L’interface est localisée ; les présentations et documents des Skills sont traduits automatiquement par IA dans votre langue. Le code, les commandes, chemins et termes techniques restent en version originale, et le document de l’auteur fait foi.
Explorer les Skills
Données et recherche · SKILL.md

exploiting-excessive-data-exposure-in-api

exploiting-excessive-data-exposure-in-api

测试 API 是否存在过度数据暴露,即端点返回的数据超出客户端应用程序实际需要的量,依赖前端过滤敏感字段。测试人员拦截 API 响应并分析其中泄露的 PII、内部标识符、调试信息或 UI 不显示但 API 传输的敏感业务数据。对应 OWASP API3:2023 对象属性级授权破坏。适用于 API 数据泄露测试、过度数据暴露、响应过滤绕过或 API 过度获取等请求场景。

Independent GitHub snapshot · Automated classification · Original documentation governs

Documentation de la Skill

GitHub

name: exploiting-excessive-data-exposure-in-api description: > 测试 API 是否存在过度数据暴露,即端点返回的数据超出客户端应用程序实际需要的量,依赖前端过滤敏感字段。测试人员拦截 API 响应并分析其中泄露的 PII、内部标识符、调试信息或 UI 不显示但 API 传输的敏感业务数据。对应 OWASP API3:2023 对象属性级授权破坏。适用于 API 数据泄露测试、过度数据暴露、响应过滤绕过或 API 过度获取等请求场景。 domain: cybersecurity subdomain: api-security tags: [api-security, owasp, data-exposure, rest-security, pii-leakage] version: 1.0.0 author: mahipal license: Apache-2.0

利用 API 中的过度数据暴露漏洞

适用场景

  • 测试前端显示数据子集但 API 响应包含额外字段的 API
  • 评估移动应用程序 API,这些 API 的响应是为多种客户端类型设计的,可能包含多余数据
  • 识别 API 响应中泄露的 PII,包括 UI 未显示的电子邮件地址、电话号码、社会安全号码或支付数据
  • 测试 GraphQL API,客户端可以请求包括敏感属性在内的任意字段
  • 评估微服务重构后,内部服务间数据是否泄露到公共端点

不适用于:未获得书面授权的情况。数据暴露测试涉及捕获和分析潜在的敏感个人数据。

前置条件

  • 书面授权,注明目标 API 端点和范围
  • Burp Suite Professional 或 mitmproxy 配置为拦截代理
  • 两个不同权限级别的测试账户(普通用户和管理员)
  • 浏览器开发者工具或移动端代理设置,用于流量捕获
  • Python 3.10+,安装 requests 和 json 库
  • API 文档(OpenAPI 规范),用于与实际响应进行比较

工作流程

步骤 1:响应 Schema 发现

将文档化的 API 响应与实际响应进行比较:

import requests
import json

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

# 获取资源并分析所有返回字段
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()
        # 递归提取所有字段名
        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)} 个字段:")
        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 = []
    # 检查值中的敏感数据模式
    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]
            })

    # 检查敏感字段名
    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

# 扫描所有端点响应
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"[发现] {f['endpoint']}: {f['type']} - {f.get('pattern', f.get('field_name'))}")

步骤 3:比较 UI 显示字段与 API 响应字段

# UI 显示的字段(从前端应用程序观察到的)
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"},
}

# API 实际返回的字段
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):
            # 处理分页响应
            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} - 多余字段(UI 未显示):")
            for field in sorted(excess):
                print(f"  - {field}")

步骤 4:测试相关端点中的用户对象暴露

# 许多 API 在订单、评论等响应中嵌入完整的用户对象
endpoints_with_user_objects = [
    "/orders",          # 每个订单可能包含完整的卖方/买方个人资料
    "/comments",        # 评论可能包含完整的作者个人资料
    "/reviews",         # 评价可能暴露评价者详情
    "/transactions",    # 交易可能包含对方信息
    "/team/members",    # 团队列表可能暴露过多成员数据
]

for path in endpoints_with_user_objects:
    resp = requests.get(f"{BASE_URL}{path}", headers=headers)
    if resp.status_code == 200:
        text = resp.text
        # 检查嵌套对象中的用户数据泄露
        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"[过度暴露] {path} 暴露了用户字段:{user_fields_found}")

步骤 5:GraphQL 过度获取分析

# GraphQL 允许客户端请求任何可用字段
GRAPHQL_URL = f"{BASE_URL}/graphql"

# 内省查询以发现 User 类型上的所有字段
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("通过 GraphQL 可用的 User 字段:")
    for f in fields:
        sensitivity = "敏感" if f["name"] in SENSITIVE_FIELD_NAMES else "普通"
        print(f"  {f['name']} ({f['type']['name']}) [{sensitivity}]")

# 尝试查询敏感字段
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("[严重] GraphQL 无限制地暴露了敏感用户字段")

步骤 6:调试和内部数据泄露

# 测试响应中的调试信息
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"[信息泄露] 响应头 {h}: {resp.headers.get(h)}")

# 测试错误响应中是否包含堆栈跟踪
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"[调试泄露] {method} {path} -> {resp.status_code}: 包含堆栈跟踪或查询信息")

核心概念

术语定义
过度数据暴露(Excessive Data Exposure)API 返回的数据字段超出客户端需要的量,依赖前端过滤向用户隐藏敏感信息
过度获取(Over-Fetching)请求或接收超出特定操作所需的数据量,在返回固定响应 Schema 的 REST API 中很常见
响应过滤(Response Filtering)客户端过滤 API 响应数据以只显示相关字段,由于完整响应可被拦截,这不提供任何安全保障
对象属性级授权(Object Property Level Authorization)OWASP API3:2023 - 确保用户只能读取/写入他们被授权访问的对象属性
PII 泄露(PII Leakage)API 响应中意外暴露个人身份信息,包括姓名、邮件地址、地址、社会安全号码或财务数据
Schema 验证(Schema Validation)强制 API 响应符合定义的 Schema,在传输前去除未授权字段

工具与系统

  • Burp Suite Professional:拦截 API 响应并使用 Comparer 工具对比预期与实际响应 Schema
  • mitmproxy:可编写脚本的代理,支持基于 Python 的内容检测脚本进行自动化响应分析
  • OWASP ZAP:被动扫描器,自动检测请求头、错误消息和响应体中的信息披露
  • Postman:使用测试脚本比较文档化的响应 Schema 与实际 API 响应
  • jq:命令行 JSON 处理器,用于从 API 响应中提取和分析特定字段

常见场景

场景:移动银行 API 数据暴露评估

场景背景:某移动银行应用程序的 API 向移动客户端返回完整的账户对象,而应用只显示账户昵称和余额。该 API 同时被 iOS 和 Android 应用以及 Web 门户访问。

方法:

  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 保护传输中的数据,但不能防止已认证客户端访问)

输出格式

## 发现:账户和交易 API 中存在过度数据暴露

**ID**: API-DATA-001
**严重性**: 高(CVSS 7.1)
**OWASP API**: API3:2023 - 对象属性级授权破坏
**受影响端点**:
  - GET /api/v1/accounts
  - GET /api/v1/transactions
  - GET /api/v1/users/me

**描述**:
API 向客户端返回完整的数据库对象,包含移动应用 UI 未显示的
敏感字段。移动应用在客户端过滤这些字段,但通过拦截 API 响应
可以完全访问它们。这暴露了任何已认证用户的 SSN 片段、内部
风险评分和 KYC 验证数据。

**发现的多余字段**:
- /accounts: routing_number, account_holder_ssn_last4, internal_risk_score,
  kyc_verification_status, linked_external_accounts(共 18 个多余字段)
- /transactions: merchant_id, terminal_id, authorization_code,
  processor_response(共 12 个多余字段)
- /users/me: last_login_ip, mfa_backup_codes_remaining, credit_score_band

**影响**:
已认证用户可以提取敏感财务数据、内部风险评估以及应用程序
本不打算暴露的 PII。结合 BOLA 漏洞,可以为所有用户提取这些数据。

**修复建议**:
1. 在服务器端使用 DTO/视图模型实施响应过滤,只包含客户端所需的字段
2. 对每个端点每个角色使用 GraphQL 字段级授权或 REST 响应 Schema
3. 在序列化层从 API 响应中移除敏感字段
4. 在 API 网关中实施响应 Schema 验证,去除未记录的字段
5. 添加自动化测试验证响应 Schema 与文档一致
Skills enregistrées