一句话总结:CDP 的 Security 和 Audits 域可以让你编程地管理浏览器安全策略——处理证书警告、监控混合内容、捕获 CSP 违规,以及控制权限授予。
目录
- 为什么用 CDP 管理安全策略
- 处理证书错误
- 监控混合内容
- 捕获 CSP 违规
- 管理权限
- 实战:自动化安全测试
- 常见踩坑与最佳实践
为什么用 CDP 管理安全策略
在自动化测试中,证书错误和混合内容经常导致测试中断。CDP 提供了编程管理方案:
| 功能 |
手动处理 |
CDP 方案 |
| 自签名证书 |
手动点击”高级→继续” |
✅ Security.setIgnoreCertificateErrors |
| 混合内容警告 |
手动允许加载 |
✅ Security.enable 监控 |
| CSP 违规 |
看 Console 面板 |
✅ Audits 域捕获 |
| 权限授予 |
手动点击允许 |
✅ Browser.setPermission |
| 安全事件 |
❌ 无自动化 |
✅ 完整事件流 |
处理证书错误
忽略证书错误
最常用的功能——在测试环境中忽略自签名证书:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| import asyncio import websockets import json
CDP_URL = "ws://127.0.0.1:9222/devtools/browser/..." CMD_ID = [0]
async def cdp(ws, session_id, method, params=None): CMD_ID[0] += 1; cmd_id = CMD_ID[0] await ws.send(json.dumps({"sessionId": session_id, "id": cmd_id, "method": method, "params": params or {}})) async for msg in ws: resp = json.loads(msg) if resp.get("id") == cmd_id: return resp.get("result", {})
async def connect_page(ws): targets = await cdp(ws, None, "Target.getTargets") target_id = targets["targetInfos"][0]["targetId"] session = await cdp(ws, None, "Target.attachToTarget", { "targetId": target_id, "flatten": True }) return session["sessionId"]
async def ignore_certificate_errors(ws, session_id, ignore=True): """ 设置是否忽略证书错误 在导航到使用自签名证书的 HTTPS 页面时非常有用 """ return await cdp(ws, session_id, "Security.setIgnoreCertificateErrors", { "ignore": ignore })
async def navigate_with_insecure_cert(ws, session_id, url): """导航到证书有问题的 HTTPS 页面""" await ignore_certificate_errors(ws, session_id, True) result = await cdp(ws, session_id, "Page.navigate", {"url": url}) await asyncio.sleep(3) print(f"已忽略证书错误,导航到: {url}") return result
|
监控安全状态变化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| async def enable_security_monitoring(ws, session_id): """启用安全事件监控""" return await cdp(ws, session_id, "Security.enable")
async def monitor_security_state(ws, session_id, duration=30): """监控页面安全状态变化""" await enable_security_monitoring(ws, session_id) events = [] start = asyncio.get_event_loop().time() while (asyncio.get_event_loop().time() - start) < duration: try: msg = await asyncio.wait_for(ws.__anext__(), timeout=1) data = json.loads(msg) method = data.get("method", "") if method == "Security.securityStateChanged": p = data["params"] state = { "security_state": p.get("securityState", ""), "scheme_cryptographic": p.get("schemeCryptographic", False), "explanations": p.get("explanations", []), } events.append(state) print(f"[安全状态] {state['security_state']}") for exp in p.get("explanations", []): print(f" - {exp.get('summary', '')}") elif method == "Security.certificateError": p = data["params"] print(f"[证书错误] {p.get('errorType', '')}: {p.get('requestURL', '')}") events.append(("certificate_error", p)) await cdp(ws, session_id, "Security.handleCertificateError", { "eventId": p.get("eventId"), "action": "continue" }) except asyncio.TimeoutError: continue return events
|
监控混合内容
混合内容(Mixed Content)指 HTTPS 页面加载 HTTP 资源。CDP 可以监控这些警告:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| async def monitor_mixed_content(ws, session_id, duration=30): """监控混合内容事件""" await cdp(ws, session_id, "Network.enable") await enable_security_monitoring(ws, session_id) mixed = [] start = asyncio.get_event_loop().time() while (asyncio.get_event_loop().time() - start) < duration: try: msg = await asyncio.wait_for(ws.__anext__(), timeout=1) data = json.loads(msg) method = data.get("method", "") if method == "Security.securityStateChanged": for exp in data["params"].get("explanations", []): if "mixed" in exp.get("summary", "").lower(): mixed.append({ "summary": exp.get("summary"), "description": exp.get("description", ""), "url": exp.get("url", "") }) print(f"[混合内容] {exp.get('summary', '')}") elif method == "Network.loadingFailed": p = data["params"] if p.get("blockedReason") == "mixed-content": mixed.append({ "type": "blocked", "url": p.get("url", ""), "error": p.get("errorText", "") }) print(f"[混合内容被拦截] {p.get('url', '')}") except asyncio.TimeoutError: continue return mixed
|
捕获 CSP 违规
Content Security Policy(CSP)违规可以通过 Audits 域或 Security 域捕获:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| async def enable_csp_monitoring(ws, session_id): """启用 CSP 违规监控""" await enable_security_monitoring(ws, session_id) await cdp(ws, session_id, "Console.enable")
async def monitor_csp_violations(ws, session_id, duration=30): """监控 CSP 违规""" violations = [] start = asyncio.get_event_loop().time() while (asyncio.get_event_loop().time() - start) < duration: try: msg = await asyncio.wait_for(ws.__anext__(), timeout=1) data = json.loads(msg) method = data.get("method", "") if method == "Console.messageAdded": msg_data = data["params"]["message"] text = msg_data.get("text", "") if "CSP" in text or "Content Security Policy" in text: violations.append({ "text": text, "url": msg_data.get("url", ""), "line": msg_data.get("line", 0), }) print(f"[CSP 违规] {text[:100]}") elif method == "Network.loadingFailed": p = data["params"] if p.get("blockedReason") in ("csp", "content-type"): violations.append({ "type": "blocked_by_csp", "url": p.get("url", ""), "reason": p.get("blockedReason"), }) print(f"[CSP 阻止] {p.get('url', '')}") except asyncio.TimeoutError: continue return violations
|
管理权限
编程授予/拒绝权限
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| async def set_permission(ws, session_id, permission_name, setting="granted"): """ 设置浏览器权限 - permission_name: 权限名称(geolocation, camera, microphone, notifications, etc.) - setting: granted, denied, prompt """ return await cdp(ws, session_id, "Browser.setPermission", { "permission": {"name": permission_name}, "setting": setting })
async def grant_common_permissions(ws, session_id): """授予常用的浏览器权限""" permissions = [ "geolocation", "camera", "microphone", "notifications", "midi", "midiSysex", "clipboardRead", "clipboardWrite", ] for perm in permissions: await set_permission(ws, session_id, perm, "granted") print(f"已授予 {len(permissions)} 个权限")
async def reset_permissions(ws, session_id): """重置所有权限为默认状态""" return await cdp(ws, session_id, "Browser.resetPermissions")
async def set_permission_for_origin(ws, session_id, origin, permission_name, setting="granted"): """为指定域名设置权限""" return await cdp(ws, session_id, "Browser.setPermission", { "origin": origin, "permission": {"name": permission_name}, "setting": setting })
|
权限列表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| """ CDP 支持的权限名称: - geolocation - 地理位置 - camera - 摄像头 - microphone - 麦克风 - notifications - 通知 - midi - MIDI - midiSysex - MIDI SysEx - clipboardRead - 剪贴板读取 - clipboardWrite - 剪贴板写入 - backgroundSync - 后台同步 - backgroundFetch - 后台获取 - persistentStorage - 持久存储 - push - 推送通知 - vibrate - 振动 - bluetooth - 蓝牙 - usb - USB - windowPlacement - 窗口放置 """
|
实战:自动化安全测试
综合运用以上技术,构建自动化安全扫描器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
| async def security_audit(ws, session_id, url): """ 对页面进行安全审计 返回结构化安全报告 """ print(f"开始安全审计: {url}") report = { "url": url, "security_state": None, "mixed_content": [], "csp_violations": [], "certificate_issues": [], "permissions": {} } await enable_security_monitoring(ws, session_id) await cdp(ws, session_id, "Network.enable") await cdp(ws, session_id, "Console.enable") events = [] async def collector(): while True: try: msg = await asyncio.wait_for(ws.__anext__(), timeout=0.5) data = json.loads(msg) events.append(data) except asyncio.TimeoutError: break except Exception: break collector_task = asyncio.create_task(collector()) await cdp(ws, session_id, "Page.navigate", {"url": url}) await asyncio.sleep(5) collector_task.cancel() for data in events: method = data.get("method", "") params = data.get("params", {}) if method == "Security.securityStateChanged": report["security_state"] = params.get("securityState") for exp in params.get("explanations", []): if "mixed" in exp.get("summary", "").lower(): report["mixed_content"].append(exp["summary"]) elif method == "Network.loadingFailed": if params.get("blockedReason") in ("csp", "mixed-content"): report["csp_violations"].append({ "url": params.get("url"), "reason": params.get("blockedReason") }) elif method == "Console.messageAdded": text = params.get("message", {}).get("text", "") if "CSP" in text: report["csp_violations"].append({"text": text[:100]}) print(f"\n=== 安全审计报告 ===") print(f"URL: {url}") print(f"安全状态: {report['security_state']}") print(f"混合内容: {len(report['mixed_content'])} 条") print(f"CSP 违规: {len(report['csp_violations'])} 条") return report
|
自动化证书过期检查
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| async def check_certificate_info(ws, session_id, url): """检查目标网站的证书信息""" await cdp(ws, session_id, "Page.navigate", {"url": url}) await asyncio.sleep(3) await enable_security_monitoring(ws, session_id) cert_info = {} for _ in range(10): try: msg = await asyncio.wait_for(ws.__anext__(), timeout=1) data = json.loads(msg) if data.get("method") == "Security.securityStateChanged": p = data["params"] cert_info = { "state": p.get("securityState"), "explanations": [ e.get("summary") for e in p.get("explanations", []) ] } break except asyncio.TimeoutError: continue return cert_info
|
常见踩坑与最佳实践
踩坑 1:setIgnoreCertificateErrors 必须在导航前设置
1 2 3 4 5 6 7
| await cdp(ws, session_id, "Page.navigate", {"url": "https://self-signed.bad"}) await ignore_certificate_errors(ws, session_id, True)
await ignore_certificate_errors(ws, session_id, True) await cdp(ws, session_id, "Page.navigate", {"url": "https://self-signed.bad"})
|
踩坑 2:权限设置需要 HTTPS
大多数权限 API 要求在 HTTPS 页面下才能调用:
1 2 3 4 5
| await set_permission(ws, session_id, "geolocation", "granted")
|
踩坑 3:Security.enable 会触发 securityStateChanged
踩坑 4:证书错误处理有两种方式
1 2 3 4 5
| await cdp(ws, session_id, "Security.setIgnoreCertificateErrors", {"ignore": True})
|
最佳实践清单
| 注意点 |
建议 |
| 证书忽略 |
导航前设置 setIgnoreCertificateErrors |
| 权限授予 |
SSL 页面才能生效 |
| CSP 监控 |
Console + Network 域同时监控 |
| 混合内容 |
Security.securityStateChanged 中包含 |
| 权限重置 |
测试结束后调用 resetPermissions |
| 安全审计 |
组合 Security + Network + Console 域 |
完整参考:CDP 安全管理类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| class CDPSecurityManager: def __init__(self, ws, session_id): self.ws = ws; self.session_id = session_id; self._cmd_id = 0 async def _cmd(self, method, params=None): self._cmd_id += 1 await self.ws.send(json.dumps({ "sessionId": self.session_id, "id": self._cmd_id, "method": method, "params": params or {} })) async for resp in self.ws: data = json.loads(resp) if data.get("id") == self._cmd_id: return data.get("result", {}) async def ignore_cert_errors(self, ignore=True): await self._cmd("Security.setIgnoreCertificateErrors", {"ignore": ignore}) async def set_permission(self, name, setting="granted", origin=None): params = {"permission": {"name": name}, "setting": setting} if origin: params["origin"] = origin await self._cmd("Browser.setPermission", params) async def reset_permissions(self): await self._cmd("Browser.resetPermissions") async def audit(self, url): await self.ignore_cert_errors(True) await self._cmd("Security.enable") await self._cmd("Network.enable") await self._cmd("Page.navigate", {"url": url}) await asyncio.sleep(3) issues = [] try: while True: msg = await asyncio.wait_for(ws.__anext__(), timeout=2) data = json.loads(msg) m = data.get("method", "") if m in ("Security.securityStateChanged", "Network.loadingFailed", "Console.messageAdded"): issues.append(data) except (asyncio.TimeoutError, Exception): pass return issues
|
使用示例:
1 2 3 4 5 6 7 8 9
| async with websockets.connect(CDP_URL) as ws: session_id = await connect_page(ws) sec = CDPSecurityManager(ws, session_id) await sec.set_permission("geolocation", "granted") await sec.ignore_cert_errors(True) issues = await sec.audit("https://example.com") print(f"发现 {len(issues)} 个安全问题")
|
总结:CDP 的 Security 和 Browser 域让你编程管理浏览器的安全策略——忽略证书错误、监控混合内容和 CSP 违规、授予和重置权限。这在自动化测试和安全审计中非常有用。
上一篇回顾:CDP Frame 管理指南:用 Python 处理 iframe 与跨域框架。
下一篇预告:CDP WebSocket 调试指南:用 Python 拦截与检查 WebSocket 帧。