opencode-api-security-testing 2.0.0 → 2.1.1
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 +30 -24
- package/SKILL.md +1797 -0
- package/core/advanced_recon.py +788 -0
- package/core/agentic_analyzer.py +445 -0
- package/core/analyzers/api_parser.py +210 -0
- package/core/analyzers/response_analyzer.py +212 -0
- package/core/analyzers/sensitive_finder.py +184 -0
- package/core/api_fuzzer.py +422 -0
- package/core/api_interceptor.py +525 -0
- package/core/api_parser.py +955 -0
- package/core/browser_tester.py +479 -0
- package/core/cloud_storage_tester.py +1330 -0
- package/core/collectors/__init__.py +23 -0
- package/core/collectors/api_path_finder.py +300 -0
- package/core/collectors/browser_collect.py +645 -0
- package/core/collectors/browser_collector.py +411 -0
- package/core/collectors/http_client.py +111 -0
- package/core/collectors/js_collector.py +490 -0
- package/core/collectors/js_parser.py +780 -0
- package/core/collectors/url_collector.py +319 -0
- package/core/context_manager.py +682 -0
- package/core/deep_api_tester_v35.py +844 -0
- package/core/deep_api_tester_v55.py +366 -0
- package/core/dynamic_api_analyzer.py +532 -0
- package/core/http_client.py +179 -0
- package/core/models.py +296 -0
- package/core/orchestrator.py +890 -0
- package/core/prerequisite.py +227 -0
- package/core/reasoning_engine.py +1042 -0
- package/core/response_classifier.py +606 -0
- package/core/runner.py +938 -0
- package/core/scan_engine.py +599 -0
- package/core/skill_executor.py +435 -0
- package/core/skill_executor_v2.py +670 -0
- package/core/skill_executor_v3.py +704 -0
- package/core/smart_analyzer.py +687 -0
- package/core/strategy_pool.py +707 -0
- package/core/testers/auth_tester.py +264 -0
- package/core/testers/idor_tester.py +200 -0
- package/core/testers/sqli_tester.py +211 -0
- package/core/testing_loop.py +655 -0
- package/core/utils/base_path_dict.py +255 -0
- package/core/utils/payload_lib.py +167 -0
- package/core/utils/ssrf_detector.py +220 -0
- package/core/verifiers/vuln_verifier.py +536 -0
- package/package.json +17 -13
- package/references/asset-discovery.md +119 -612
- package/references/graphql-guidance.md +65 -641
- package/references/intake.md +84 -0
- package/references/report-template.md +131 -38
- package/references/rest-guidance.md +55 -526
- package/references/severity-model.md +52 -264
- package/references/test-matrix.md +65 -263
- package/references/validation.md +53 -400
- package/scripts/postinstall.js +46 -0
- package/src/index.ts +259 -275
- package/agents/cyber-supervisor.md +0 -55
- package/agents/probing-miner.md +0 -42
- package/agents/resource-specialist.md +0 -31
- package/commands/api-security-testing-scan.md +0 -59
- package/commands/api-security-testing-test.md +0 -49
- package/commands/api-security-testing.md +0 -72
- package/tsconfig.json +0 -17
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
"""
|
|
2
|
+
漏洞验证器 - 多维度验证
|
|
3
|
+
确认发现的漏洞是否真实,排除误报
|
|
4
|
+
|
|
5
|
+
验证维度:
|
|
6
|
+
1. 响应类型维度:JSON vs HTML vs Empty vs Redirect
|
|
7
|
+
2. 状态码维度:200 vs 4xx vs 5xx
|
|
8
|
+
3. 响应长度维度:长度变化检测
|
|
9
|
+
4. WAF拦截维度:WAF/安全设备拦截检测
|
|
10
|
+
5. 敏感信息维度:敏感字段泄露检测
|
|
11
|
+
6. SQL注入维度:SQL错误特征检测
|
|
12
|
+
7. IDOR维度:用户数据越权检测
|
|
13
|
+
8. 一致性维度:多次请求响应一致性检测
|
|
14
|
+
9. 时间维度:时间盲注检测
|
|
15
|
+
10. 业务数据维度:业务数据真实性检测
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import requests
|
|
19
|
+
import json
|
|
20
|
+
import time
|
|
21
|
+
import re
|
|
22
|
+
|
|
23
|
+
requests.packages.urllib3.disable_warnings()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def vuln_verifier(config):
|
|
27
|
+
"""
|
|
28
|
+
多维度漏洞验证
|
|
29
|
+
|
|
30
|
+
输入:
|
|
31
|
+
type: "sqli" | "idor" | "auth_bypass" | "info_leak"
|
|
32
|
+
original_request: object
|
|
33
|
+
suspicious_response: object
|
|
34
|
+
baseline_response?: object - 基线响应(正常请求的响应)
|
|
35
|
+
|
|
36
|
+
输出:
|
|
37
|
+
verified: boolean
|
|
38
|
+
is_false_positive: boolean
|
|
39
|
+
reason: string
|
|
40
|
+
dimensions: object - 各维度验证结果
|
|
41
|
+
"""
|
|
42
|
+
vuln_type = config.get('type')
|
|
43
|
+
original_request = config.get('original_request', {})
|
|
44
|
+
suspicious_response = config.get('suspicious_response', {})
|
|
45
|
+
baseline_response = config.get('baseline_response', {})
|
|
46
|
+
|
|
47
|
+
# 收集各维度验证结果
|
|
48
|
+
dimensions = {}
|
|
49
|
+
is_false_positive = False
|
|
50
|
+
reasons = []
|
|
51
|
+
|
|
52
|
+
# ========== 维度1: 响应类型验证 ==========
|
|
53
|
+
resp_type_result = verify_response_type(suspicious_response)
|
|
54
|
+
dimensions['response_type'] = resp_type_result
|
|
55
|
+
if resp_type_result['is_waf_or_block']:
|
|
56
|
+
is_false_positive = True
|
|
57
|
+
reasons.append(f"响应类型为{resp_type_result['type']},可能是拦截")
|
|
58
|
+
|
|
59
|
+
# ========== 维度2: 状态码验证 ==========
|
|
60
|
+
status_result = verify_status_code(suspicious_response)
|
|
61
|
+
dimensions['status_code'] = status_result
|
|
62
|
+
|
|
63
|
+
# ========== 维度3: 响应长度验证 ==========
|
|
64
|
+
length_result = verify_response_length(suspicious_response, baseline_response)
|
|
65
|
+
dimensions['response_length'] = length_result
|
|
66
|
+
if length_result['is_empty']:
|
|
67
|
+
is_false_positive = True
|
|
68
|
+
reasons.append("响应为空或过短")
|
|
69
|
+
|
|
70
|
+
# ========== 维度4: WAF拦截验证 ==========
|
|
71
|
+
waf_result = verify_waf_block(suspicious_response)
|
|
72
|
+
dimensions['waf_block'] = waf_result
|
|
73
|
+
if waf_result['detected']:
|
|
74
|
+
is_false_positive = True
|
|
75
|
+
reasons.append(f"检测到WAF/拦截: {waf_result['reason']}")
|
|
76
|
+
|
|
77
|
+
# ========== 维度5: 敏感信息验证 ==========
|
|
78
|
+
sensitive_result = verify_sensitive_info(suspicious_response)
|
|
79
|
+
dimensions['sensitive_info'] = sensitive_result
|
|
80
|
+
|
|
81
|
+
# ========== 维度6: 一致性验证 ==========
|
|
82
|
+
if baseline_response:
|
|
83
|
+
consistency_result = verify_consistency(suspicious_response, baseline_response)
|
|
84
|
+
dimensions['consistency'] = consistency_result
|
|
85
|
+
if not consistency_result['is_consistent']:
|
|
86
|
+
is_false_positive = True
|
|
87
|
+
reasons.append("响应与基线不一致,可能是偶发")
|
|
88
|
+
|
|
89
|
+
# ========== 维度7: 基于漏洞类型的专项验证 ==========
|
|
90
|
+
if vuln_type == 'sqli':
|
|
91
|
+
# SQL注入专项验证
|
|
92
|
+
sqli_result = verify_sqli_dimension(suspicious_response)
|
|
93
|
+
dimensions['sqli'] = sqli_result
|
|
94
|
+
|
|
95
|
+
# SQL注入必须满足:响应是JSON + 包含SQL错误特征
|
|
96
|
+
if not sqli_result['has_sql_error']:
|
|
97
|
+
is_false_positive = True
|
|
98
|
+
reasons.append("未发现SQL错误特征")
|
|
99
|
+
|
|
100
|
+
elif vuln_type == 'idor':
|
|
101
|
+
# IDOR专项验证
|
|
102
|
+
idor_result = verify_idor_dimension(suspicious_response)
|
|
103
|
+
dimensions['idor'] = idor_result
|
|
104
|
+
|
|
105
|
+
# IDOR必须满足:返回业务数据 + 不同ID返回不同数据
|
|
106
|
+
if not idor_result['has_user_data']:
|
|
107
|
+
is_false_positive = True
|
|
108
|
+
reasons.append("未发现用户/业务数据")
|
|
109
|
+
|
|
110
|
+
elif vuln_type == 'auth_bypass':
|
|
111
|
+
# 认证绕过专项验证
|
|
112
|
+
auth_result = verify_auth_bypass(suspicious_response)
|
|
113
|
+
dimensions['auth_bypass'] = auth_result
|
|
114
|
+
|
|
115
|
+
if not auth_result['bypassed']:
|
|
116
|
+
is_false_positive = True
|
|
117
|
+
reasons.append("认证绕过未确认")
|
|
118
|
+
|
|
119
|
+
elif vuln_type == 'info_leak':
|
|
120
|
+
# 信息泄露专项验证
|
|
121
|
+
leak_result = verify_info_leak(suspicious_response)
|
|
122
|
+
dimensions['info_leak'] = leak_result
|
|
123
|
+
|
|
124
|
+
if not leak_result['has_leak']:
|
|
125
|
+
is_false_positive = True
|
|
126
|
+
reasons.append("未发现信息泄露")
|
|
127
|
+
|
|
128
|
+
# ========== 最终判定 ==========
|
|
129
|
+
verified = not is_false_positive
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
'verified': verified,
|
|
133
|
+
'is_false_positive': is_false_positive,
|
|
134
|
+
'reason': '; '.join(reasons) if reasons else '验证通过' if verified else '多项验证失败',
|
|
135
|
+
'dimensions': dimensions
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# ========== 维度1: 响应类型验证 ==========
|
|
140
|
+
def verify_response_type(response):
|
|
141
|
+
"""
|
|
142
|
+
验证响应类型
|
|
143
|
+
维度说明:JSON=真实API,HTML=WAF/路由/拦截
|
|
144
|
+
"""
|
|
145
|
+
body = response.get('body', '')
|
|
146
|
+
headers = response.get('headers', {})
|
|
147
|
+
content_type = headers.get('Content-Type', '')
|
|
148
|
+
|
|
149
|
+
result = {
|
|
150
|
+
'type': 'unknown',
|
|
151
|
+
'is_json': False,
|
|
152
|
+
'is_html': False,
|
|
153
|
+
'is_empty': False,
|
|
154
|
+
'is_waf_or_block': False
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
# 检查是否是JSON
|
|
158
|
+
if 'application/json' in content_type.lower():
|
|
159
|
+
try:
|
|
160
|
+
json.loads(body)
|
|
161
|
+
result['is_json'] = True
|
|
162
|
+
result['type'] = 'json'
|
|
163
|
+
except:
|
|
164
|
+
pass
|
|
165
|
+
|
|
166
|
+
if not result['is_json'] and body.strip().startswith('{'):
|
|
167
|
+
try:
|
|
168
|
+
json.loads(body)
|
|
169
|
+
result['is_json'] = True
|
|
170
|
+
result['type'] = 'json'
|
|
171
|
+
except:
|
|
172
|
+
pass
|
|
173
|
+
|
|
174
|
+
# 检查是否是HTML(可能是WAF/路由/拦截)
|
|
175
|
+
if '<!doctype html>' in body.lower() or '<html' in body.lower():
|
|
176
|
+
result['is_html'] = True
|
|
177
|
+
result['type'] = 'html'
|
|
178
|
+
result['is_waf_or_block'] = True
|
|
179
|
+
|
|
180
|
+
# 检查是否为空
|
|
181
|
+
if len(body) < 50:
|
|
182
|
+
result['is_empty'] = True
|
|
183
|
+
result['type'] = 'empty'
|
|
184
|
+
result['is_waf_or_block'] = True
|
|
185
|
+
|
|
186
|
+
return result
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# ========== 维度2: 状态码验证 ==========
|
|
190
|
+
def verify_status_code(response):
|
|
191
|
+
"""
|
|
192
|
+
验证状态码
|
|
193
|
+
维度说明:200=成功,4xx=客户端错误,5xx=服务端错误
|
|
194
|
+
"""
|
|
195
|
+
status = response.get('status', 0)
|
|
196
|
+
|
|
197
|
+
result = {
|
|
198
|
+
'status': status,
|
|
199
|
+
'is_success': status == 200,
|
|
200
|
+
'is_client_error': 400 <= status < 500,
|
|
201
|
+
'is_server_error': status >= 500,
|
|
202
|
+
'is_redirect': 300 <= status < 400
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return result
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# ========== 维度3: 响应长度验证 ==========
|
|
209
|
+
def verify_response_length(response, baseline=None):
|
|
210
|
+
"""
|
|
211
|
+
验证响应长度
|
|
212
|
+
维度说明:过短可能是拦截,过长可能是完整数据
|
|
213
|
+
"""
|
|
214
|
+
body = response.get('body', '')
|
|
215
|
+
length = len(body)
|
|
216
|
+
|
|
217
|
+
result = {
|
|
218
|
+
'length': length,
|
|
219
|
+
'is_empty': length < 50,
|
|
220
|
+
'is_reasonable': 50 <= length <= 50000,
|
|
221
|
+
'is_too_long': length > 50000
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
# 与基线对比
|
|
225
|
+
if baseline:
|
|
226
|
+
baseline_length = len(baseline.get('body', ''))
|
|
227
|
+
if baseline_length > 0:
|
|
228
|
+
diff_ratio = abs(length - baseline_length) / max(length, baseline_length)
|
|
229
|
+
result['diff_ratio'] = diff_ratio
|
|
230
|
+
result['significantly_different'] = diff_ratio > 0.8
|
|
231
|
+
|
|
232
|
+
return result
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# ========== 维度4: WAF拦截验证 ==========
|
|
236
|
+
def verify_waf_block(response):
|
|
237
|
+
"""
|
|
238
|
+
验证WAF/安全设备拦截
|
|
239
|
+
维度说明:检测响应是否为WAF或安全设备的拦截页面
|
|
240
|
+
"""
|
|
241
|
+
body = response.get('body', '').lower()
|
|
242
|
+
headers = response.get('headers', {})
|
|
243
|
+
|
|
244
|
+
waf_indicators = {
|
|
245
|
+
'waf': ['waf', 'web应用防火墙', '安全防护', '防火墙'],
|
|
246
|
+
'block': ['拦截', 'blocked', 'forbidden', '访问受限', 'blocked by'],
|
|
247
|
+
'security': ['安全中心', '安全狗', '云盾', '安全狗'],
|
|
248
|
+
'cdn': ['cdn', 'content filter']
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
detected_type = None
|
|
252
|
+
detected_indicators = []
|
|
253
|
+
|
|
254
|
+
for wtype, indicators in waf_indicators.items():
|
|
255
|
+
for indicator in indicators:
|
|
256
|
+
if indicator in body:
|
|
257
|
+
detected_type = wtype
|
|
258
|
+
detected_indicators.append(indicator)
|
|
259
|
+
|
|
260
|
+
# 检查header
|
|
261
|
+
if not detected_type:
|
|
262
|
+
x_powered = headers.get('X-Powered-By', '').lower()
|
|
263
|
+
for wtype, indicators in waf_indicators.items():
|
|
264
|
+
for indicator in indicators:
|
|
265
|
+
if indicator in x_powered:
|
|
266
|
+
detected_type = wtype
|
|
267
|
+
detected_indicators.append(indicator)
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
'detected': detected_type is not None,
|
|
271
|
+
'type': detected_type,
|
|
272
|
+
'indicators': detected_indicators,
|
|
273
|
+
'reason': f"检测到{detected_type}: {', '.join(detected_indicators)}" if detected_type else None
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
# ========== 维度5: 敏感信息验证 ==========
|
|
278
|
+
def verify_sensitive_info(response):
|
|
279
|
+
"""
|
|
280
|
+
验证敏感信息泄露
|
|
281
|
+
维度说明:检测响应中是否包含敏感字段
|
|
282
|
+
"""
|
|
283
|
+
body = response.get('body', '')
|
|
284
|
+
|
|
285
|
+
sensitive_fields = {
|
|
286
|
+
'password': r'password["\']?\s*[:=]\s*["\']([^"\']{1,50})["\']',
|
|
287
|
+
'token': r'(?:token|Token|TOKEN)["\']?\s*[:=]\s*["\']([^"\']{10,200})["\']',
|
|
288
|
+
'jwt': r'eyJ[A-Za-z0-9-_]+\.eyJ[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+',
|
|
289
|
+
'api_key': r'api[_-]?key["\']?\s*[:=]\s*["\']([^"\']{10,100})["\']',
|
|
290
|
+
'secret': r'secret["\']?\s*[:=]\s*["\']([^"\']{1,100})["\']',
|
|
291
|
+
'phone': r'1[3-9]\d{9}',
|
|
292
|
+
'email': r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
found = {}
|
|
296
|
+
|
|
297
|
+
try:
|
|
298
|
+
# 尝试JSON解析
|
|
299
|
+
data = json.loads(body)
|
|
300
|
+
body_for_search = json.dumps(data)
|
|
301
|
+
except:
|
|
302
|
+
body_for_search = str(body)
|
|
303
|
+
|
|
304
|
+
for field_name, pattern in sensitive_fields.items():
|
|
305
|
+
matches = re.findall(pattern, body_for_search, re.I)
|
|
306
|
+
if matches:
|
|
307
|
+
# 过滤测试数据
|
|
308
|
+
filtered = [m for m in matches if not is_test_data(m)]
|
|
309
|
+
if filtered:
|
|
310
|
+
found[field_name] = len(filtered)
|
|
311
|
+
except:
|
|
312
|
+
pass
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
'found': found,
|
|
316
|
+
'has_sensitive': len(found) > 0,
|
|
317
|
+
'count': sum(found.values())
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
# ========== 维度6: 一致性验证 ==========
|
|
322
|
+
def verify_consistency(response1, response2):
|
|
323
|
+
"""
|
|
324
|
+
验证响应一致性
|
|
325
|
+
维度说明:多次请求响应应该一致,否则可能是偶发
|
|
326
|
+
"""
|
|
327
|
+
status1 = response1.get('status', 0)
|
|
328
|
+
status2 = response2.get('status', 0)
|
|
329
|
+
body1 = response1.get('body', '')
|
|
330
|
+
body2 = response2.get('body', '')
|
|
331
|
+
|
|
332
|
+
result = {
|
|
333
|
+
'status_consistent': status1 == status2,
|
|
334
|
+
'body_similar': True,
|
|
335
|
+
'is_consistent': True
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
# 检查状态码
|
|
339
|
+
if status1 != status2:
|
|
340
|
+
result['status_consistent'] = False
|
|
341
|
+
result['is_consistent'] = False
|
|
342
|
+
|
|
343
|
+
# 检查body相似度
|
|
344
|
+
if body1 and body2:
|
|
345
|
+
len1, len2 = len(body1), len(body2)
|
|
346
|
+
if max(len1, len2) > 0:
|
|
347
|
+
diff_ratio = abs(len1 - len2) / max(len1, len2)
|
|
348
|
+
result['body_diff_ratio'] = diff_ratio
|
|
349
|
+
if diff_ratio > 0.5:
|
|
350
|
+
result['body_similar'] = False
|
|
351
|
+
result['is_consistent'] = False
|
|
352
|
+
|
|
353
|
+
return result
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
# ========== 维度7: SQL注入专项验证 ==========
|
|
357
|
+
def verify_sqli_dimension(response):
|
|
358
|
+
"""
|
|
359
|
+
SQL注入专项验证
|
|
360
|
+
必须满足:响应是JSON + 包含SQL错误特征
|
|
361
|
+
"""
|
|
362
|
+
body = response.get('body', '')
|
|
363
|
+
|
|
364
|
+
# SQL错误特征
|
|
365
|
+
sql_errors = [
|
|
366
|
+
'sql syntax', 'syntax error', 'mysql', 'postgresql', 'oracle',
|
|
367
|
+
'sqlite', 'sqlstate', 'microsoft sql', 'sql error', 'sqlsrv',
|
|
368
|
+
'odbc driver', 'mariadb', 'access denied for',
|
|
369
|
+
'sql_injection', 'sql injection'
|
|
370
|
+
]
|
|
371
|
+
|
|
372
|
+
has_sql_error = False
|
|
373
|
+
detected_error = None
|
|
374
|
+
|
|
375
|
+
body_lower = body.lower()
|
|
376
|
+
for error in sql_errors:
|
|
377
|
+
if error in body_lower:
|
|
378
|
+
has_sql_error = True
|
|
379
|
+
detected_error = error
|
|
380
|
+
break
|
|
381
|
+
|
|
382
|
+
# 检查是否是JSON格式的错误响应
|
|
383
|
+
is_json_error = False
|
|
384
|
+
try:
|
|
385
|
+
data = json.loads(body)
|
|
386
|
+
if isinstance(data, dict):
|
|
387
|
+
msg = str(data.get('msg', '')).lower()
|
|
388
|
+
for error in sql_errors:
|
|
389
|
+
if error in msg:
|
|
390
|
+
is_json_error = True
|
|
391
|
+
break
|
|
392
|
+
# 检查code是否为错误码
|
|
393
|
+
if data.get('code') and data.get('code') not in [200, 0, '200', '0']:
|
|
394
|
+
is_json_error = True
|
|
395
|
+
except:
|
|
396
|
+
pass
|
|
397
|
+
|
|
398
|
+
return {
|
|
399
|
+
'has_sql_error': has_sql_error,
|
|
400
|
+
'error_type': detected_error,
|
|
401
|
+
'is_json_error': is_json_error,
|
|
402
|
+
'confirmed': has_sql_error and is_json_error
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
# ========== 维度8: IDOR专项验证 ==========
|
|
407
|
+
def verify_idor_dimension(response):
|
|
408
|
+
"""
|
|
409
|
+
IDOR专项验证
|
|
410
|
+
必须满足:返回业务数据 + 数据随ID变化
|
|
411
|
+
"""
|
|
412
|
+
body = response.get('body', '')
|
|
413
|
+
|
|
414
|
+
# 业务字段
|
|
415
|
+
business_fields = [
|
|
416
|
+
'user', 'username', 'userId', 'user_id', 'name',
|
|
417
|
+
'phone', 'mobile', 'email',
|
|
418
|
+
'order', 'orderId', 'order_no', 'orderNo',
|
|
419
|
+
'balance', 'amount', 'money',
|
|
420
|
+
'id', '_id', 'createBy', 'create_by'
|
|
421
|
+
]
|
|
422
|
+
|
|
423
|
+
has_user_data = False
|
|
424
|
+
matched_fields = []
|
|
425
|
+
|
|
426
|
+
try:
|
|
427
|
+
data = json.loads(body)
|
|
428
|
+
data_str = json.dumps(data).lower()
|
|
429
|
+
|
|
430
|
+
for field in business_fields:
|
|
431
|
+
if field.lower() in data_str:
|
|
432
|
+
has_user_data = True
|
|
433
|
+
matched_fields.append(field)
|
|
434
|
+
except:
|
|
435
|
+
pass
|
|
436
|
+
|
|
437
|
+
return {
|
|
438
|
+
'has_user_data': has_user_data,
|
|
439
|
+
'matched_fields': matched_fields,
|
|
440
|
+
'confirmed': has_user_data
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
# ========== 维度9: 认证绕过专项验证 ==========
|
|
445
|
+
def verify_auth_bypass(response):
|
|
446
|
+
"""
|
|
447
|
+
认证绕过专项验证
|
|
448
|
+
必须满足:返回token或session
|
|
449
|
+
"""
|
|
450
|
+
body = response.get('body', '')
|
|
451
|
+
|
|
452
|
+
has_token = False
|
|
453
|
+
has_session = False
|
|
454
|
+
|
|
455
|
+
# token特征
|
|
456
|
+
token_patterns = [
|
|
457
|
+
r'token["\']?\s*[:=]\s*["\']([^"\']{10,})',
|
|
458
|
+
r'access_token["\']?\s*[:=]\s*["\']([^"\']{10,})',
|
|
459
|
+
r'session_id["\']?\s*[:=]\s*["\']([^"\']{10,})',
|
|
460
|
+
r'Bearer\s+[a-zA-Z0-9\-_\.]+'
|
|
461
|
+
]
|
|
462
|
+
|
|
463
|
+
for pattern in token_patterns:
|
|
464
|
+
if re.search(pattern, body, re.I):
|
|
465
|
+
has_token = True
|
|
466
|
+
break
|
|
467
|
+
|
|
468
|
+
# 检查是否是成功登录的响应
|
|
469
|
+
try:
|
|
470
|
+
data = json.loads(body)
|
|
471
|
+
if data.get('success') == True or data.get('code') == 0:
|
|
472
|
+
if data.get('token') or data.get('data', {}).get('token'):
|
|
473
|
+
has_token = True
|
|
474
|
+
except:
|
|
475
|
+
pass
|
|
476
|
+
|
|
477
|
+
return {
|
|
478
|
+
'has_token': has_token,
|
|
479
|
+
'has_session': has_session,
|
|
480
|
+
'bypassed': has_token
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
# ========== 维度10: 信息泄露专项验证 ==========
|
|
485
|
+
def verify_info_leak(response):
|
|
486
|
+
"""
|
|
487
|
+
信息泄露专项验证
|
|
488
|
+
必须满足:返回非公开的业务信息
|
|
489
|
+
"""
|
|
490
|
+
body = response.get('body', '')
|
|
491
|
+
|
|
492
|
+
# 非公开信息特征
|
|
493
|
+
private_info = [
|
|
494
|
+
'password', 'secret', 'api_key', 'apiKey',
|
|
495
|
+
'token', 'session', 'private',
|
|
496
|
+
'phone', 'email', 'id_card', '身份证'
|
|
497
|
+
]
|
|
498
|
+
|
|
499
|
+
found = []
|
|
500
|
+
|
|
501
|
+
body_lower = body.lower()
|
|
502
|
+
for info in private_info:
|
|
503
|
+
if info in body_lower:
|
|
504
|
+
found.append(info)
|
|
505
|
+
|
|
506
|
+
return {
|
|
507
|
+
'found': found,
|
|
508
|
+
'has_leak': len(found) > 0,
|
|
509
|
+
'confirmed': len(found) > 0
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
# ========== 辅助函数 ==========
|
|
514
|
+
def is_test_data(value):
|
|
515
|
+
"""判断是否是测试数据"""
|
|
516
|
+
test_patterns = [
|
|
517
|
+
'test', 'TEST', 'Test',
|
|
518
|
+
'xxx', 'xxx.xxx',
|
|
519
|
+
'null', 'undefined',
|
|
520
|
+
'example', 'sample',
|
|
521
|
+
'placeholder', 'demo'
|
|
522
|
+
]
|
|
523
|
+
value_lower = str(value).lower()
|
|
524
|
+
return any(t in value_lower for t in test_patterns)
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
if __name__ == '__main__':
|
|
528
|
+
# 测试
|
|
529
|
+
result = vuln_verifier({
|
|
530
|
+
'type': 'sqli',
|
|
531
|
+
'original_request': {'url': 'http://example.com/api/login', 'method': 'POST'},
|
|
532
|
+
'suspicious_response': {'status': 200, 'body': '{"error": "success"}'}
|
|
533
|
+
})
|
|
534
|
+
print(f"Verified: {result['verified']}")
|
|
535
|
+
print(f"False Positive: {result['is_false_positive']}")
|
|
536
|
+
print(f"Dimensions: {list(result['dimensions'].keys())}")
|
package/package.json
CHANGED
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-api-security-testing",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "API Security Testing Plugin for OpenCode - Automated vulnerability scanning and penetration testing",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "
|
|
7
|
-
"types": "
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"types": "src/index.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"src/",
|
|
10
|
+
"core/",
|
|
11
|
+
"references/",
|
|
12
|
+
"SKILL.md",
|
|
13
|
+
"scripts/"
|
|
14
|
+
],
|
|
14
15
|
"scripts": {
|
|
15
|
-
"
|
|
16
|
-
"dev": "bun build src/index.ts --outdir dist --target bun --format esm --watch",
|
|
17
|
-
"typecheck": "tsc --noEmit"
|
|
16
|
+
"postinstall": "node scripts/postinstall.js"
|
|
18
17
|
},
|
|
19
18
|
"keywords": [
|
|
20
19
|
"opencode",
|
|
@@ -24,8 +23,13 @@
|
|
|
24
23
|
"pentest",
|
|
25
24
|
"vulnerability-scanning"
|
|
26
25
|
],
|
|
27
|
-
"author": "",
|
|
26
|
+
"author": "steveopen1",
|
|
28
27
|
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/steveopen1/skill-play"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/steveopen1/skill-play/tree/main/agent-plugins/OPENCODE/api-security-testing",
|
|
29
33
|
"peerDependencies": {
|
|
30
34
|
"@opencode-ai/plugin": "^1.1.19",
|
|
31
35
|
"@opencode-ai/sdk": "^1.1.19"
|