opencode-api-security-testing 3.0.10 → 3.0.11
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 +74 -0
- 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 +1 -1
- package/references/README.md +72 -0
- package/references/asset-discovery.md +119 -0
- package/references/fuzzing-patterns.md +129 -0
- package/references/graphql-guidance.md +108 -0
- package/references/intake.md +84 -0
- package/references/pua-agent.md +192 -0
- package/references/report-template.md +156 -0
- package/references/rest-guidance.md +76 -0
- package/references/severity-model.md +76 -0
- package/references/test-matrix.md +86 -0
- package/references/validation.md +78 -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
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Browser Automation Tester - 浏览器动态测试引擎
|
|
4
|
+
支持 Playwright/Puppeteer/Selenium 多引擎
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import time
|
|
9
|
+
import re
|
|
10
|
+
import warnings
|
|
11
|
+
from typing import Dict, List, Tuple, Optional, Any
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from enum import Enum
|
|
14
|
+
|
|
15
|
+
warnings.filterwarnings("ignore")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BrowserEngine(Enum):
|
|
19
|
+
PLAYWRIGHT = "playwright"
|
|
20
|
+
PUPPETEER = "puppeteer"
|
|
21
|
+
SELENIUM = "selenium"
|
|
22
|
+
NONE = "none"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class XSSResult:
|
|
27
|
+
vuln_type: str
|
|
28
|
+
payload: str
|
|
29
|
+
location: str
|
|
30
|
+
sink: Optional[str] = None
|
|
31
|
+
severity: str = "medium"
|
|
32
|
+
evidence: str = ""
|
|
33
|
+
url: str = ""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class BrowserTestConfig:
|
|
38
|
+
target_url: str
|
|
39
|
+
engine: BrowserEngine = BrowserEngine.NONE
|
|
40
|
+
headless: bool = True
|
|
41
|
+
timeout: int = 30000
|
|
42
|
+
wait_for_selector: Optional[str] = None
|
|
43
|
+
screenshot_on_error: bool = False
|
|
44
|
+
console_log: bool = False
|
|
45
|
+
user_data_dir: Optional[str] = None
|
|
46
|
+
proxy: Optional[str] = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class BrowserAutomationTester:
|
|
50
|
+
"""
|
|
51
|
+
浏览器动态测试引擎
|
|
52
|
+
|
|
53
|
+
支持:
|
|
54
|
+
- DOM XSS 检测
|
|
55
|
+
- SPA 路由测试
|
|
56
|
+
- 表单交互测试
|
|
57
|
+
- JavaScript 执行检测
|
|
58
|
+
- Cookie/Session 测试
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(self, config: BrowserTestConfig):
|
|
62
|
+
self.config = config
|
|
63
|
+
self.engine = config.engine
|
|
64
|
+
self.results: List[XSSResult] = []
|
|
65
|
+
self._browser = None
|
|
66
|
+
self._context = None
|
|
67
|
+
self._page = None
|
|
68
|
+
self._init_engine()
|
|
69
|
+
|
|
70
|
+
def _init_engine(self):
|
|
71
|
+
"""初始化浏览器引擎"""
|
|
72
|
+
if self.engine == BrowserEngine.PLAYWRIGHT:
|
|
73
|
+
self._init_playwright()
|
|
74
|
+
elif self.engine == BrowserEngine.SELENIUM:
|
|
75
|
+
self._init_selenium()
|
|
76
|
+
elif self.engine == BrowserEngine.PUPPETEER:
|
|
77
|
+
self._init_puppeteer()
|
|
78
|
+
else:
|
|
79
|
+
print("[!] No browser engine available. Install: pip install playwright")
|
|
80
|
+
|
|
81
|
+
def _init_playwright(self):
|
|
82
|
+
"""初始化 Playwright"""
|
|
83
|
+
try:
|
|
84
|
+
from playwright.sync_api import sync_playwright
|
|
85
|
+
self._playwright = sync_playwright().start()
|
|
86
|
+
self._browser = self._playwright.chromium.launch(
|
|
87
|
+
headless=self.config.headless,
|
|
88
|
+
args=["--no-sandbox", "--disable-dev-shm-usage"]
|
|
89
|
+
)
|
|
90
|
+
self._context = self._browser.new_context(
|
|
91
|
+
ignore_https_errors=True,
|
|
92
|
+
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
|
93
|
+
)
|
|
94
|
+
self._page = self._context.new_page()
|
|
95
|
+
if self.config.console_log:
|
|
96
|
+
self._page.on("console", lambda msg: print(f"[Browser Console] {msg.text}"))
|
|
97
|
+
print("[+] Playwright initialized successfully")
|
|
98
|
+
except ImportError:
|
|
99
|
+
print("[!] Playwright not installed. Run: pip install playwright && playwright install chromium")
|
|
100
|
+
self.engine = BrowserEngine.NONE
|
|
101
|
+
except Exception as e:
|
|
102
|
+
print(f"[!] Playwright init failed: {e}")
|
|
103
|
+
self.engine = BrowserEngine.NONE
|
|
104
|
+
|
|
105
|
+
def _init_selenium(self):
|
|
106
|
+
"""初始化 Selenium"""
|
|
107
|
+
try:
|
|
108
|
+
from selenium import webdriver
|
|
109
|
+
from selenium.webdriver.chrome.options import Options
|
|
110
|
+
from selenium.webdriver.chrome.service import Service
|
|
111
|
+
|
|
112
|
+
options = Options()
|
|
113
|
+
if self.config.headless:
|
|
114
|
+
options.add_argument("--headless")
|
|
115
|
+
options.add_argument("--no-sandbox")
|
|
116
|
+
options.add_argument("--disable-dev-shm-usage")
|
|
117
|
+
if self.config.proxy:
|
|
118
|
+
options.add_argument(f"--proxy-server={self.config.proxy}")
|
|
119
|
+
|
|
120
|
+
self._driver = webdriver.Chrome(options=options)
|
|
121
|
+
self._driver.set_page_load_timeout(self.config.timeout / 1000)
|
|
122
|
+
print("[+] Selenium initialized successfully")
|
|
123
|
+
except ImportError:
|
|
124
|
+
print("[!] Selenium not installed. Run: pip install selenium")
|
|
125
|
+
self.engine = BrowserEngine.NONE
|
|
126
|
+
except Exception as e:
|
|
127
|
+
print(f"[!] Selenium init failed: {e}")
|
|
128
|
+
self.engine = BrowserEngine.NONE
|
|
129
|
+
|
|
130
|
+
def _init_puppeteer(self):
|
|
131
|
+
"""初始化 Puppeteer (通过 pyppeteer)"""
|
|
132
|
+
try:
|
|
133
|
+
import asyncio
|
|
134
|
+
from pyppeteer import launch
|
|
135
|
+
asyncio.get_event_loop().run_until_complete(
|
|
136
|
+
self._init_puppeteer_async()
|
|
137
|
+
)
|
|
138
|
+
except ImportError:
|
|
139
|
+
print("[!] pyppeteer not installed. Run: pip install pyppeteer")
|
|
140
|
+
self.engine = BrowserEngine.NONE
|
|
141
|
+
except Exception as e:
|
|
142
|
+
print(f"[!] Puppeteer init failed: {e}")
|
|
143
|
+
self.engine = BrowserEngine.NONE
|
|
144
|
+
|
|
145
|
+
async def _init_puppeteer_async(self):
|
|
146
|
+
self._browser = await launch(
|
|
147
|
+
headless=self.config.headless,
|
|
148
|
+
args=["--no-sandbox", "--disable-dev-shm-usage"]
|
|
149
|
+
)
|
|
150
|
+
self._page = await self._browser.newPage()
|
|
151
|
+
print("[+] Puppeteer initialized successfully")
|
|
152
|
+
|
|
153
|
+
def test_dom_xss(self, payloads: List[Dict]) -> List[XSSResult]:
|
|
154
|
+
"""
|
|
155
|
+
测试 DOM XSS 漏洞
|
|
156
|
+
|
|
157
|
+
检测 sinks: innerHTML, eval, document.write, location.href, etc.
|
|
158
|
+
"""
|
|
159
|
+
if self.engine == BrowserEngine.NONE:
|
|
160
|
+
return []
|
|
161
|
+
|
|
162
|
+
dom_sinks = {
|
|
163
|
+
"innerHTML": r'innerHTML\s*=',
|
|
164
|
+
"outerHTML": r'outerHTML\s*=',
|
|
165
|
+
"insertAdjacentHTML": r'insertAdjacentHTML',
|
|
166
|
+
"document.write": r'document\.write',
|
|
167
|
+
"eval": r'eval\(',
|
|
168
|
+
"setTimeout": r'setTimeout\s*\(',
|
|
169
|
+
"setInterval": r'setInterval\s*\(',
|
|
170
|
+
"Function": r'new\s+Function\(',
|
|
171
|
+
"location.href": r'location\.href',
|
|
172
|
+
"location.hash": r'location\.hash',
|
|
173
|
+
"location.search": r'location\.search',
|
|
174
|
+
"document.cookie": r'document\.cookie',
|
|
175
|
+
"localStorage": r'localStorage\.',
|
|
176
|
+
"sessionStorage": r'sessionStorage\.',
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
xss_payloads = [p for p in payloads if p.get("type") in ["dom", "reflected", "polyglot"]]
|
|
180
|
+
|
|
181
|
+
print(f"[*] Testing {len(xss_payloads)} DOM XSS payloads...")
|
|
182
|
+
|
|
183
|
+
for payload_data in xss_payloads:
|
|
184
|
+
payload = payload_data.get("payload", "")
|
|
185
|
+
sink = payload_data.get("sink", "")
|
|
186
|
+
|
|
187
|
+
for url_pattern, source in [
|
|
188
|
+
(f"{self.config.target_url}?input={payload}", "URL parameter"),
|
|
189
|
+
(f"{self.config.target_url}#{payload}", "URL fragment"),
|
|
190
|
+
(f"{self.config.target_url}?redirect={payload}", "redirect param"),
|
|
191
|
+
]:
|
|
192
|
+
try:
|
|
193
|
+
self._navigate_and_check(url_pattern, payload, sink, dom_sinks)
|
|
194
|
+
except Exception as e:
|
|
195
|
+
print(f"[!] Error testing payload: {e}")
|
|
196
|
+
|
|
197
|
+
return self.results
|
|
198
|
+
|
|
199
|
+
def _navigate_and_check(self, url: str, payload: str, sink: str, dom_sinks: Dict):
|
|
200
|
+
"""导航到 URL 并检查 XSS"""
|
|
201
|
+
if self.engine == BrowserEngine.PLAYWRIGHT:
|
|
202
|
+
self._page.goto(url, timeout=self.config.timeout)
|
|
203
|
+
self._page.wait_for_load_state("networkidle", timeout=self.config.timeout)
|
|
204
|
+
time.sleep(1)
|
|
205
|
+
|
|
206
|
+
content = self._page.content()
|
|
207
|
+
|
|
208
|
+
for sink_pattern, sink_name in dom_sinks.items():
|
|
209
|
+
if re.search(sink_pattern, content, re.IGNORECASE):
|
|
210
|
+
if payload in content or any(x in content for x in ["<script", "<img", "<svg"]):
|
|
211
|
+
self.results.append(XSSResult(
|
|
212
|
+
vuln_type="DOM XSS",
|
|
213
|
+
payload=payload,
|
|
214
|
+
location="client-side",
|
|
215
|
+
sink=sink_name,
|
|
216
|
+
severity="high",
|
|
217
|
+
evidence=f"Sink '{sink_name}' found with payload",
|
|
218
|
+
url=url
|
|
219
|
+
))
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
def test_spa_routing(self, routes: List[str]) -> Dict[str, Any]:
|
|
223
|
+
"""
|
|
224
|
+
测试 SPA 路由安全性
|
|
225
|
+
|
|
226
|
+
1. 测试路由参数中的 XSS
|
|
227
|
+
2. 测试认证绕过
|
|
228
|
+
3. 测试敏感端点访问
|
|
229
|
+
"""
|
|
230
|
+
if self.engine == BrowserEngine.NONE:
|
|
231
|
+
return {"error": "No browser engine"}
|
|
232
|
+
|
|
233
|
+
results = {
|
|
234
|
+
"routes_tested": [],
|
|
235
|
+
"xss_found": [],
|
|
236
|
+
"auth_bypass": [],
|
|
237
|
+
"sensitive_access": []
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
test_params = ["<script>alert(1)</script>", "' Or '1'='1", "../../../etc/passwd"]
|
|
241
|
+
|
|
242
|
+
for route in routes:
|
|
243
|
+
full_url = f"{self.config.target_url}{route}"
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
if self.engine == BrowserEngine.PLAYWRIGHT:
|
|
247
|
+
self._page.goto(full_url, timeout=self.config.timeout)
|
|
248
|
+
self._page.wait_for_load_state("networkidle")
|
|
249
|
+
time.sleep(1)
|
|
250
|
+
|
|
251
|
+
results["routes_tested"].append(route)
|
|
252
|
+
|
|
253
|
+
content = self._page.content()
|
|
254
|
+
for param in test_params:
|
|
255
|
+
if param in content:
|
|
256
|
+
results["xss_found"].append({
|
|
257
|
+
"route": route,
|
|
258
|
+
"payload": param
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
title = self._page.title()
|
|
262
|
+
if "admin" in title.lower() or "dashboard" in title.lower():
|
|
263
|
+
results["sensitive_access"].append({
|
|
264
|
+
"route": route,
|
|
265
|
+
"title": title
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
except Exception as e:
|
|
269
|
+
print(f"[!] Error testing route {route}: {e}")
|
|
270
|
+
|
|
271
|
+
return results
|
|
272
|
+
|
|
273
|
+
def test_form_interaction(self, form_selectors: List[Dict]) -> List[XSSResult]:
|
|
274
|
+
"""
|
|
275
|
+
测试表单交互 XSS
|
|
276
|
+
|
|
277
|
+
填写表单并提交,检测存储型 XSS
|
|
278
|
+
"""
|
|
279
|
+
if self.engine == BrowserEngine.NONE:
|
|
280
|
+
return []
|
|
281
|
+
|
|
282
|
+
results = []
|
|
283
|
+
|
|
284
|
+
xss_test_payloads = [
|
|
285
|
+
"<script>alert(1)</script>",
|
|
286
|
+
"<img src=x onerror=alert(1)>",
|
|
287
|
+
"javascript:alert(1)",
|
|
288
|
+
"'; alert(1); //"
|
|
289
|
+
]
|
|
290
|
+
|
|
291
|
+
for form in form_selectors:
|
|
292
|
+
selector = form.get("selector")
|
|
293
|
+
fields = form.get("fields", {})
|
|
294
|
+
|
|
295
|
+
for payload in xss_test_payloads:
|
|
296
|
+
try:
|
|
297
|
+
if self.engine == BrowserEngine.PLAYWRIGHT:
|
|
298
|
+
self._page.goto(self.config.target_url, timeout=self.config.timeout)
|
|
299
|
+
self._page.wait_for_load_state("networkidle")
|
|
300
|
+
|
|
301
|
+
for field_name, field_type in fields.items():
|
|
302
|
+
if field_type == "text":
|
|
303
|
+
self._page.fill(f"input[name='{field_name}']", payload)
|
|
304
|
+
elif field_type == "textarea":
|
|
305
|
+
self._page.fill(f"textarea[name='{field_name}']", payload)
|
|
306
|
+
|
|
307
|
+
if "submit" in fields:
|
|
308
|
+
self._page.click(f"button[type='submit'], input[type='submit']")
|
|
309
|
+
time.sleep(2)
|
|
310
|
+
|
|
311
|
+
content = self._page.content()
|
|
312
|
+
if payload in content:
|
|
313
|
+
results.append(XSSResult(
|
|
314
|
+
vuln_type="Stored XSS",
|
|
315
|
+
payload=payload,
|
|
316
|
+
location=f"Form: {selector}",
|
|
317
|
+
severity="critical",
|
|
318
|
+
evidence="Payload persisted after submission"
|
|
319
|
+
))
|
|
320
|
+
|
|
321
|
+
except Exception as e:
|
|
322
|
+
print(f"[!] Form test error: {e}")
|
|
323
|
+
|
|
324
|
+
return results
|
|
325
|
+
|
|
326
|
+
def test_javascript_execution(self, payload: str) -> Tuple[bool, str]:
|
|
327
|
+
"""
|
|
328
|
+
测试 JavaScript 是否被执行
|
|
329
|
+
|
|
330
|
+
使用 console.log 配合 CDP 获取执行结果
|
|
331
|
+
"""
|
|
332
|
+
if self.engine == BrowserEngine.NONE:
|
|
333
|
+
return False, "No browser engine"
|
|
334
|
+
|
|
335
|
+
test_payloads = [
|
|
336
|
+
f"<script>console.log('XSS_TEST_{payload[:20]}')</script>",
|
|
337
|
+
f"<img src=x onerror='console.log(\"XSS_TEST_{payload[:20]}\")'>",
|
|
338
|
+
f"<svg onload='console.log(\"XSS_TEST_{payload[:20]}\")'>"
|
|
339
|
+
]
|
|
340
|
+
|
|
341
|
+
for test_payload in test_payloads:
|
|
342
|
+
try:
|
|
343
|
+
if self.engine == BrowserEngine.PLAYWRIGHT:
|
|
344
|
+
console_messages = []
|
|
345
|
+
|
|
346
|
+
def handle_console(msg):
|
|
347
|
+
if "XSS_TEST" in msg.text:
|
|
348
|
+
console_messages.append(msg.text)
|
|
349
|
+
|
|
350
|
+
self._page.on("console", handle_console)
|
|
351
|
+
|
|
352
|
+
test_url = f"{self.config.target_url}?input={test_payload}"
|
|
353
|
+
self._page.goto(test_url, timeout=self.config.timeout)
|
|
354
|
+
self._page.wait_for_load_state("networkidle")
|
|
355
|
+
time.sleep(2)
|
|
356
|
+
|
|
357
|
+
if console_messages:
|
|
358
|
+
return True, f"JS executed: {console_messages}"
|
|
359
|
+
|
|
360
|
+
except Exception as e:
|
|
361
|
+
continue
|
|
362
|
+
|
|
363
|
+
return False, "No JS execution detected"
|
|
364
|
+
|
|
365
|
+
def capture_network_requests(self) -> List[Dict]:
|
|
366
|
+
"""捕获网络请求,用于分析 API 调用"""
|
|
367
|
+
if self.engine == BrowserEngine.PLAYWRIGHT:
|
|
368
|
+
requests = []
|
|
369
|
+
|
|
370
|
+
def handle_request(request):
|
|
371
|
+
requests.append({
|
|
372
|
+
"url": request.url,
|
|
373
|
+
"method": request.method,
|
|
374
|
+
"headers": dict(request.headers),
|
|
375
|
+
"post_data": request.post_data
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
self._page.on("request", handle_request)
|
|
379
|
+
self._page.reload()
|
|
380
|
+
self._page.wait_for_load_state("networkidle")
|
|
381
|
+
|
|
382
|
+
return requests
|
|
383
|
+
return []
|
|
384
|
+
|
|
385
|
+
def get_client_storage(self) -> Dict[str, Dict]:
|
|
386
|
+
"""获取客户端存储 (localStorage, sessionStorage, cookies)"""
|
|
387
|
+
if self.engine == BrowserEngine.PLAYWRIGHT:
|
|
388
|
+
return {
|
|
389
|
+
"localStorage": self._page.evaluate("() => JSON.stringify(localStorage)"),
|
|
390
|
+
"sessionStorage": self._page.evaluate("() => JSON.stringify(sessionStorage)"),
|
|
391
|
+
"cookies": [dict(c) for c in self._context.cookies()]
|
|
392
|
+
}
|
|
393
|
+
return {}
|
|
394
|
+
|
|
395
|
+
def close(self):
|
|
396
|
+
"""关闭浏览器"""
|
|
397
|
+
try:
|
|
398
|
+
if self._browser:
|
|
399
|
+
self._browser.close()
|
|
400
|
+
if hasattr(self, '_driver') and self._driver:
|
|
401
|
+
self._driver.quit()
|
|
402
|
+
except:
|
|
403
|
+
pass
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def auto_detect_engine() -> BrowserEngine:
|
|
407
|
+
"""自动检测可用的浏览器引擎"""
|
|
408
|
+
try:
|
|
409
|
+
from playwright.sync_api import sync_playwright
|
|
410
|
+
return BrowserEngine.PLAYWRIGHT
|
|
411
|
+
except ImportError:
|
|
412
|
+
pass
|
|
413
|
+
|
|
414
|
+
try:
|
|
415
|
+
from selenium import webdriver
|
|
416
|
+
return BrowserEngine.SELENIUM
|
|
417
|
+
except ImportError:
|
|
418
|
+
pass
|
|
419
|
+
|
|
420
|
+
return BrowserEngine.NONE
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def create_tester(target_url: str, headless: bool = True) -> Optional[BrowserAutomationTester]:
|
|
424
|
+
"""创建浏览器测试器,自动选择可用引擎"""
|
|
425
|
+
engine = auto_detect_engine()
|
|
426
|
+
|
|
427
|
+
if engine == BrowserEngine.NONE:
|
|
428
|
+
print("[!] No browser automation library found.")
|
|
429
|
+
print("[*] Install one of:")
|
|
430
|
+
print(" - Playwright: pip install playwright && playwright install chromium")
|
|
431
|
+
print(" - Selenium: pip install selenium")
|
|
432
|
+
return None
|
|
433
|
+
|
|
434
|
+
config = BrowserTestConfig(
|
|
435
|
+
target_url=target_url,
|
|
436
|
+
engine=engine,
|
|
437
|
+
headless=headless,
|
|
438
|
+
timeout=30000,
|
|
439
|
+
console_log=False
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
return BrowserAutomationTester(config)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
# CLI interface
|
|
446
|
+
if __name__ == "__main__":
|
|
447
|
+
import argparse
|
|
448
|
+
|
|
449
|
+
parser = argparse.ArgumentParser(description="Browser Automation Security Tester")
|
|
450
|
+
parser.add_argument("--target", required=True, help="Target URL")
|
|
451
|
+
parser.add_argument("--type", choices=["dom", "spa", "form", "all"], default="all")
|
|
452
|
+
parser.add_argument("--payloads", help="Path to payloads JSON file")
|
|
453
|
+
parser.add_argument("--routes", nargs="+", help="SPA routes to test")
|
|
454
|
+
parser.add_argument("--headless", action="store_true", default=True)
|
|
455
|
+
|
|
456
|
+
args = parser.parse_args()
|
|
457
|
+
|
|
458
|
+
tester = create_tester(args.target, headless=args.headless)
|
|
459
|
+
|
|
460
|
+
if not tester:
|
|
461
|
+
exit(1)
|
|
462
|
+
|
|
463
|
+
payloads = [
|
|
464
|
+
{"id": "xss-001", "payload": "<script>alert(1)</script>", "type": "dom"},
|
|
465
|
+
{"id": "xss-002", "payload": "<img src=x onerror=alert(1)>", "type": "reflected"},
|
|
466
|
+
]
|
|
467
|
+
|
|
468
|
+
if args.type in ["dom", "all"]:
|
|
469
|
+
print("\n[*] Running DOM XSS tests...")
|
|
470
|
+
results = tester.test_dom_xss(payloads)
|
|
471
|
+
print(f"[+] Found {len(results)} DOM XSS vulnerabilities")
|
|
472
|
+
|
|
473
|
+
if args.type in ["spa", "all"]:
|
|
474
|
+
routes = args.routes or ["/", "/admin", "/dashboard", "/login"]
|
|
475
|
+
print("\n[*] Running SPA routing tests...")
|
|
476
|
+
results = tester.test_spa_routing(routes)
|
|
477
|
+
print(f"[+] Tested {len(results.get('routes_tested', []))} routes")
|
|
478
|
+
|
|
479
|
+
tester.close()
|