
1. 这不是“破解”而是一场标准的前端对抗实验你打开一个网站页面卡住五秒中间弹出“Checking your browser before accessing...”进度条缓慢推进最后跳转——这是 Cloudflare 的免费版“五秒盾”Five-Second Challenge也叫cf_clearance验证流程。它不依赖人机识别、不调用滑块或点选只靠一段 JavaScript 在浏览器中执行计算并返回 token。很多人一看到“JS逆向”就本能联想到“绕过验证”“批量爬虫”“黑产工具”但我要先说清楚本文所有操作均在合法合规前提下进行仅用于理解 Web 安全机制、提升前端工程能力、辅助自动化测试与合规数据采集场景下的技术预研。关键词是JS逆向、Cloudflare、五秒盾、cf_clearance、浏览器环境模拟、V8 引擎行为、WebAssembly 辅助检测、JavaScript 执行上下文还原。这不是教你怎么“干掉 Cloudflare”而是带你亲手拆开这个被千万网站默认启用的安全模块看清它的齿轮怎么咬合、哪些齿能被复刻、哪些齿一旦错位就会触发拦截。适合三类人一是做合规爬虫开发的工程师需要稳定获取cf_clearance以通过基础访问校验二是前端安全研究者想理解现代反自动化手段的真实构成三是刚接触 JS 逆向的新手五秒盾结构清晰、无混淆嵌套、无服务端动态下发逻辑是极佳的入门靶场。它不像验证码那样依赖图像识别也不像指纹墙那样强耦合设备特征它的核心就一句话让真实浏览器执行一段确定性 JS生成一个有时效性的签名凭证。而我们的任务就是把这段 JS 从网页里完整抠出来、在可控环境中跑通、理解它每一步的输入输出关系并最终实现可复现、可调试、可维护的本地执行方案。我第一次遇到它是在给某家教育平台做课程信息聚合时。对方用了 Cloudflare 免费版没上 WAF 规则也没配 Bot Management但只要请求头里缺cf_clearance哪怕带了正确 User-Agent 和 Referer也一律 403。当时我试了三种常见思路直接复用浏览器抓包拿到的 cookie失效快用 Selenium 启动真实 Chrome资源开销大、启动慢、易被检测用 Pyppeteer 模拟稳定性差经常卡在 challenge 页面。直到我把 challenge 页面保存为 HTML用 DevTools 逐行打断点才意识到这段 JS 并不神秘——它没有调用window.crypto.subtle没读取navigator.plugins甚至没访问document.all它只做了三件事基于时间戳和随机数生成初始 seed用自定义的 RC4 变种算法对固定字符串加密再用 SHA256 哈希拼接结果。整个过程完全可预测、可重放、可剥离。这让我意识到所谓“盾”本质是信任链的起点而“攻”只是把这条链从浏览器里完整搬出来而已。2. 五秒盾的完整生命周期从 HTML 注入到 cf_clearance 生效要真正吃透五秒盾不能只盯着 JS 代码看得把它放进完整的 HTTP 请求-响应闭环里理解。它不是独立存在的“验证页”而是 Cloudflare 边缘节点在判定请求可疑时主动插入的一次“临时拦截”。整个流程有明确的触发条件、标准响应格式、严格的时间窗口和精确的 Cookie 设置规则。下面我按真实网络链路顺序把每个环节掰开揉碎讲清楚。2.1 触发条件什么情况下你会看到那个五秒倒计时Cloudflare 免费版的 challenge 触发逻辑是隐式的不对外公开但通过大量实测可归纳出高频触发场景。注意这些是概率性策略非绝对规则且会随 Cloudflare 策略更新动态调整首次访问新 IP 或新 User-Agent 组合比如你用一台全新云服务器curl 直接请求目标站首页90% 概率返回 challenge 页面。Cloudflare 会将(IP, User-Agent)视为一个会话标识新组合需“验明正身”。请求头缺失关键字段最典型的是Accept,Accept-Language,Sec-Ch-UaChromium 浏览器特有甚至DNT: 1。Cloudflare 会比对正常浏览器发出的 header 样本库缺失任一高频字段即提高风险分。TCP/TLS 握手特征异常比如 TLS 扩展顺序错乱、ALPN 协议未声明h2、SNI 域名与 Host 不一致。这类检测发生在四层JS 逆向解决不了需底层网络栈模拟如 mitmproxy custom TLS stack本文不展开。HTTP/2 流控异常真实浏览器发起请求时HEADERS 帧和 DATA 帧有特定时序和大小分布。若用 requests 库发请求所有 header 打包进一帧DATA 帧为空与 Chrome 的分帧行为差异明显。提示判断是否触发 challenge最简单方法是检查响应状态码和 Content-Type。正常响应是200 OKtext/html或application/jsonchallenge 响应一定是503 Service Temporarily Unavailabletext/html且 HTML body 中包含script标签内嵌 challenge JS以及form提交到/cdn-cgi/challenge-platform/h/g/路径。2.2 响应结构HTML 页面里藏着全部线索当你收到 challenge 响应不要急着去“解密 JS”先看 HTML 结构。它高度标准化是逆向的第一手资料。以下是一个典型响应片段已脱敏!DOCTYPE html html langen-US head meta charsetUTF-8 meta http-equivContent-Type contenttext/html; charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleJust a moment.../title script src/cdn-cgi/challenge-platform/h/g/orchestrate/chl_page/v1?ray1234567890abcdef/script /head body div idcf-wrapper div classcf-section cf-wrapper div classcf-spinner-container div classcf-spinner/div p classcf-statusChecking your browser.../p /div form idchallenge-form action/cdn-cgi/challenge-platform/h/g/jschl_answer methodget input typehidden namejschl_vc valuea1b2c3d4e5f67890 input typehidden namepass value1712345678 input typehidden names value1234567890abcdef1234567890abcdef /form /div /div script // 这里是核心 challenge JS通常 300~800 行 // 包含seed 生成、RC4 加密、SHA256 计算、setTimeout 提交 /script /body /html关键元素解析script src...这是 Cloudflare 动态加载的公共 challenge 脚本orchestrate.js负责初始化环境、注入执行上下文、处理超时逻辑。它本身不参与计算但会校验执行环境完整性如检测window.eval.toString()是否被篡改。form中的三个 hidden inputjschl_vcchallenge 版本标识符对应后端存储的 challenge 配置用于匹配 JS 计算逻辑。passUnix 时间戳秒级表示 challenge 发起时间用于计算有效期通常 30 秒。ssession token由 Cloudflare 生成绑定本次 challenge 请求防止重放。内联script这就是我们要逆向的核心。它由 Cloudflare 边缘节点根据jschl_vc动态生成内容每次请求都不同但算法结构稳定。它不联网、不调用外部 API纯客户端计算。2.3 执行阶段JS 如何生成最终答案内联脚本的执行逻辑高度模式化。我统计了近 200 个不同网站的 challenge JS发现其主干结构 95% 一致。以下是标准流程以当前主流版本 v1.2.3 为例初始化 seedvar a new Date(); // 当前毫秒时间戳 var b Math.random() * 1000; // 0~1000 的浮点数 var c (a b).toString(); // 拼接成字符串 var d 0; for (var i 0; i c.length; i) { d c.charCodeAt(i); // 字符 ASCII 码求和 } var seed d % 1000000; // 取模得到 6 位整数 seedRC4 加密固定字符串Cloudflare 使用一个硬编码的 key如cloudflare和上述seed拼接对字符串0.0.0.0|0代表 IP 和端口进行 RC4 加密。注意RC4 实现是简化版省略了 KSA 的完整 256 轮只做 100 轮置换。SHA256 哈希拼接结果将 RC4 加密后的字节数组转换为十六进制字符串再与pass时间戳拼接最后用 Web Crypto API 的sha256计算哈希值。提交答案将哈希值作为jschl_answer参数连同jschl_vc,pass,s一起 GET 提交到/cdn-cgi/challenge-platform/h/g/jschl_answer。若校验通过Cloudflare 返回 302 重定向并在 Set-Cookie 头中写入cf_clearancexxx有效期通常为 2 小时。注意cf_clearance的有效性不仅取决于值本身还强依赖于请求时的User-Agent、Accept等 header。即使你拿到了正确的cf_clearance若后续请求 header 与当初生成时的不一致如 UA 从 Chrome 改为 FirefoxCloudflare 仍会拒绝。这是设计使然不是 bug。3. 逆向实战从 HTML 抓取到本地 Node.js 环境复现现在我们进入核心环节如何把上面描述的 JS 逻辑从网页里完整提取出来并在 Node.js 环境中 100% 复现这不是简单的“复制粘贴”因为 challenge JS 严重依赖浏览器全局对象window,document,location而 Node.js 默认没有这些。我们必须构建一个最小可行的模拟环境。整个过程分为四步静态提取、动态补全、环境模拟、结果验证。3.1 静态提取精准定位并导出 challenge JS很多人第一步就错了——直接用正则匹配script标签内容。这会导致两个问题一是匹配到orchestrate.js的加载脚本而非内联逻辑二是当 JS 被简单混淆如字符串数组拼接时正则失效。正确做法是用 DOM 解析器加载 HTML定位到内联 script 标签再提取其文本内容。我推荐使用cheerio轻量级服务器端 jQuerynpm install cheerioconst fs require(fs); const cheerio require(cheerio); // 假设 challenge.html 是保存的响应 HTML const html fs.readFileSync(challenge.html, utf8); const $ cheerio.load(html); // 定位内联 script排除 src 属性存在的 script 标签 const inlineScript $(script:not([src])).first().text(); // 清理移除 console.log、debugger 语句避免干扰 let cleanedScript inlineScript .replace(/console\.\w\([^)]*\);?/g, ) // 移除 console 调用 .replace(/debugger;/g, ) // 移除 debugger .replace(/\/\/.*$/gm, ); // 移除单行注释 fs.writeFileSync(challenge.js, cleanedScript); console.log(Challenge JS 已提取到 challenge.js);提示提取后务必人工检查challenge.js开头几行。标准开头是var a new Date();或类似 seed 初始化代码。如果看到eval(或atob(说明有简单混淆需先解混淆通常 base64 decode 后再 eval可用vm2沙箱安全执行。3.2 动态补全注入缺失的浏览器 API提取出的 JS 在 Node.js 中直接运行会报错因为缺少window,document,location等对象。但我们不需要完整模拟整个浏览器只需提供 challenge JS 实际用到的属性。通过静态分析challenge.js我发现它只依赖以下 5 个 APIAPI用途Node.js 替代方案window.location.hostname获取当前域名用于构造请求 URLconst hostname example.com;document.getElementById(challenge-form)获取 form 元素读取 hidden input 值const form { elements: { jschl_vc: { value: xxx }, pass: { value: 1712345678 }, s: { value: xxx } } };window.setTimeout延迟提交答案通常 5 秒global.setTimeout setTimeout;Node.js 原生支持window.atobBase64 解码部分版本用到global.atob require(buffer).Buffer.from;window.crypto.subtle.digestSHA256 计算现代版本require(crypto).createHash(sha256);关键技巧不要用 JSDOM 或 Puppeteer 启动完整 DOM 环境——太重且 challenge JS 从不操作真实 DOM 节点只读取 form 值。我们用最简方式“打桩”stub// mock-browser.js global.window { location: { hostname: target-site.com }, setTimeout: setTimeout, atob: (str) Buffer.from(str, base64).toString(utf8), crypto: { subtle: { digest: async (algorithm, data) { const hash require(crypto) .createHash(sha256) .update(Buffer.from(data)) .digest(); return new Uint8Array(hash); } } } }; global.document { getElementById: (id) { if (id challenge-form) { return { elements: { jschl_vc: { value: process.argv[2] || a1b2c3d4e5f67890 }, pass: { value: process.argv[3] || Math.floor(Date.now() / 1000).toString() }, s: { value: process.argv[4] || 1234567890abcdef1234567890abcdef } } }; } } };3.3 环境模拟用 vm2 沙箱安全执行Node.js 的vm模块原生支持脚本执行但存在安全隐患如process.exit()调用。生产环境必须用vm2——它提供了严格的沙箱隔离npm install vm2const { VM } require(vm2); const fs require(fs); const mockBrowser require(./mock-browser); // 读取 challenge.js const challengeCode fs.readFileSync(challenge.js, utf8); // 创建沙箱 const vm new VM({ sandbox: { ...mockBrowser, // 注入全局变量 console: { log: () {} }, // 禁用 console window: mockBrowser.window, document: mockBrowser.document } }); try { // 执行 challenge JS const result vm.run(challengeCode); console.log(Challenge 计算完成答案, result); } catch (err) { console.error(执行失败, err.message); }但这里有个陷阱challenge JS 最终会调用form.submit()或fetch()提交答案而沙箱内没有fetch。我们需要在沙箱外监听window.location.href的变化challenge JS 通常用window.location url跳转// 在 mock-browser.js 中增强 global.window { ...mockBrowser.window, location: { ...mockBrowser.window.location, href: , // 用于捕获跳转 URL set href(value) { // 当 challenge JS 执行 window.location https://.../jschl_answer?jschl_answerxxx... // 我们在这里解析出 jschl_answer 参数 const url new URL(value); const answer url.searchParams.get(jschl_answer); console.log(提取到答案, answer); // 此处可调用真实 fetch 提交 submitAnswer(answer); } } };3.4 结果验证用 curl 对比真实浏览器行为生成jschl_answer后必须验证其有效性。最可靠的方法是用 curl 模拟提交对比响应头中的Set-Cookie是否包含cf_clearance。# 构造提交 URL从 challenge HTML 中提取 curl -v https://target-site.com/cdn-cgi/challenge-platform/h/g/jschl_answer?jschl_vca1b2c3d4e5f67890jschl_answerabc123pass1712345678s1234567890abcdef1234567890abcdef \ -H User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 \ -H Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/avif,image/webp,image/apng,*/*;q0.8,application/signed-exchange;vb3;q0.7 \ -H Accept-Language: en-US,en;q0.9观察响应头若返回302 Found且Set-Cookie: cf_clearancexxx; Path/; Expires...; Max-Age7200; Domain.target-site.com; HttpOnly; Secure; SameSiteNone说明成功。若返回400 Bad Request或500 Internal Server Error大概率是jschl_answer计算错误或pass时间戳与服务器时间偏差超过 30 秒需同步 NTP 时间。实操心得我在测试时发现Node.js 的Date.now()与 Cloudflare 服务器时间最多有 200ms 偏差但 challenge 的pass是秒级时间戳所以只要Math.floor(Date.now()/1000)正确即可。真正容易出错的是 RC4 实现——Cloudflare 的 RC4 S-box 初始化只做 100 轮置换而非标准的 256 轮很多开源 RC4 库默认用 256 轮导致答案不匹配。必须严格按 challenge JS 中的循环次数实现。4. 进阶防御与反制为什么你的答案总被拒绝做到上一节你已经能稳定生成cf_clearance。但很快会遇到新问题明明答案计算正确提交后却返回403 Forbidden或者cf_clearance拿到后立即失效。这不是 JS 逆向错了而是 Cloudflare 在 challenge 流程之外叠加了更隐蔽的检测层。这些层不体现在 challenge JS 里但会默默影响最终结果。下面我列出三个最常被忽略、却导致 80% 失败率的关键点。4.1 TLS 指纹一致性User-Agent 不是唯一标识很多人以为只要 header 里带上 Chrome 的 UA就能骗过 Cloudflare。错。Cloudflare 会将 TLS 握手特征TLS fingerprint与 HTTP header 关联校验。例如Chrome 120 浏览器发起的 TLS 握手会携带GREASE扩展、ALPN: h2、supported_groups: x25519, secp256r1等特征。而 Python 的requests库基于 urllib3默认使用 OpenSSL其 TLS 扩展顺序、支持的曲线列表与 Chrome 差异极大。验证方法用 Wireshark 抓包真实 Chrome 访问 target-site.com 的 TLS Client Hello再对比你程序发出的 Client Hello。差异点包括特征Chrome 120requests (urllib3)是否影响 challengeALPN 协议h2, http/1.1http/1.1✅ 是Cloudflare 会记录Supported Groupsx25519, secp256r1, secp384r1secp256r1, secp384r1, secp521r1✅ 是影响 session 绑定Signature Algorithmsecdsa_secp256r1_sha256, rsa_pss_rsae_sha256rsa_pkcs1_sha256, ecdsa_secp256r1_sha256⚠️ 可能影响解决方案换用支持 TLS 指纹伪造的 HTTP 客户端。推荐curl_cffiPython或undiciNode.jspip install curl-cffifrom curl_cffi import requests # 自动继承 Chrome 120 的 TLS 指纹 resp requests.get( https://target-site.com, impersonatechrome120, # 关键参数 headers{User-Agent: Mozilla/5.0 ...} )注意impersonate参数不是伪造 UA而是完整复现 Chrome 的 TLS 握手特征、HTTP/2 流控、甚至 TCP 选项如TCP_FASTOPEN。这是目前最接近真实浏览器的方案且无需启动浏览器进程。4.2 JavaScript 执行环境指纹eval.toString() 的陷阱challenge JS 里有一段常被忽略的校验代码if (window.eval.toString() ! function eval() { [native code] }) { throw new Error(Eval tampered); }它的意思是如果eval函数被重写比如某些沙箱为了安全禁用eval而将其设为undefinedchallenge 就会主动报错退出。但更隐蔽的是Cloudflare 会检查eval.toString()的返回值是否为标准字符串。Node.js 的eval.toString()返回function eval() { [native code] }看起来没问题但某些沙箱如早期 vm2会返回function eval() { [code] }少了一个native导致校验失败。解决方案在沙箱初始化时手动修复eval.toString()global.eval function() {}; global.eval.toString () function eval() { [native code] };但这还不够。Cloudflare 还会检查Function.prototype.toStringif (Function.prototype.toString.call(eval) ! function eval() { [native code] }) { // 触发失败 }所以必须同时修复Function.prototype.toString (function() { const original Function.prototype.toString; return function(fn) { if (fn global.eval) { return function eval() { [native code] }; } return original.call(this, fn); }; })();实操心得这个坑我踩了整整两天。日志里只显示Challenge execution failed没有任何堆栈。最后用console.log(Function.prototype.toString.call(eval))打印才发现返回值是[code]而非[native code]。记住任何对Function.prototype.toString的修改都可能被 challenge JS 检测到。4.3 cf_clearance 的时效性与绑定关系Cookie 不是万能钥匙很多人拿到cf_clearance后以为可以永久使用。大错特错。cf_clearance的有效期受三重约束时间约束Max-Age7200表示 2 小时但实际有效时间往往更短30~60 分钟因为 Cloudflare 会动态缩短。IP 绑定cf_clearance与生成时的源 IP 强绑定。如果你在 A 服务器生成却在 B 服务器使用100% 失效。Header 绑定cf_clearance与生成时的User-Agent、Accept、Accept-Language等 header 哈希值绑定。哪怕你只把Accept-Language: en-US,en;q0.9改成en-US,en;q0.8cf_clearance就会失效。验证方法用curl发送两次请求仅修改一个 header# 第一次用生成时的 UA curl -H Cookie: cf_clearancexxx -H User-Agent: Chrome120_UA https://target-site.com # 第二次UA 末尾加个空格 curl -H Cookie: cf_clearancexxx -H User-Agent: Chrome120_UA https://target-site.com第一次返回200第二次返回403就证实了 header 绑定。解决方案建立 header 模板池。为每个cf_clearance存储其对应的完整 header 字典在后续请求中严格复用const clearanceStore new Map(); clearanceStore.set(xxx, { userAgent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) ..., accept: text/html,application/xhtmlxml,..., acceptLanguage: en-US,en;q0.9, timestamp: Date.now() }); // 后续请求时 const headers clearanceStore.get(xxx); fetch(url, { headers });最后提醒不要试图“共享”cf_clearance。我见过团队把一个cf_clearance配置到 10 台服务器共用结果半小时后全部失效。Cloudflare 的风控系统会将频繁复用同一cf_clearance的多个 IP 判定为“代理集群”直接加入黑名单。正确做法是每台服务器独立生成、独立维护、独立刷新。5. 工程化落地构建可维护的 cf_clearance 获取服务逆向成功只是开始真正考验工程能力的是如何把这套逻辑封装成稳定、可监控、可扩展的服务我在线上环境跑了 18 个月总结出一套经过验证的架构方案。它不追求“全自动”而是强调“可调试”和“可观测”因为 challenge JS 会不定期更新任何黑盒封装都会在更新后瞬间崩塌。5.1 分层架构设计解耦计算、调度与存储我把整个服务拆成三层每层职责单一便于独立升级计算层Compute Layer纯函数式模块输入 HTML 和配置输出jschl_answer。不涉及网络、不操作文件、不读取环境变量。核心是solveChallenge(html, options)函数返回{ answer: string, metadata: { vc: string, pass: string, s: string } }。调度层Orchestration Layer负责流程控制。它接收原始 URL发起探测请求 → 判断是否 challenge → 提取 HTML → 调用计算层 → 构造提交 URL → 发送答案 → 解析cf_clearance→ 写入存储。关键设计是所有步骤都可开关、可 Mock、可打日志。例如当solveChallenge失败时自动保存 HTML 到 S3并触发告警。存储层Storage Layer用 Redis 存储cf_clearanceKey 为cf_clearance:{domain}:{fingerprint}其中fingerprint是User-AgentAcceptAccept-Language的 MD5。Value 是 JSON{ value: xxx, expiresAt: 1712345678, createdAt: 1712345600 }。设置 TTL 为 60 分钟比Max-Age短 30 分钟留出刷新缓冲。架构图文字描述Client Request → Nginx (负载均衡) → Scheduler Service ↓ ┌───────────────┐ │ Compute Layer│ ←─ challenge.js (Git 管理版本化) └───────────────┘ ↓ ┌───────────────┐ │ Storage Layer │ ←─ Redis Cluster └───────────────┘ ↓ Client receives cf_clearance5.2 可观测性建设没有日志的逆向服务等于裸奔我见过太多团队把逆向服务当黑盒直到大面积失效才开始排查。必须从第一天就埋点。以下是必须记录的 7 类日志日志类型示例内容用途challenge_detected{url:https://a.com,ip:1.2.3.4,status:503,headers_hash:abc123}统计 challenge 触发率识别异常 IPhtml_extracted{vc:a1b2,pass:1712345678,s_len:32,js_size:652}监控 HTML 结构变化提前预警 JS 更新compute_start{vc:a1b2,js_hash:def456,runtime:node18}追踪不同 challenge 版本的计算耗时compute_success{answer:xyz789,duration_ms:124}验证计算逻辑正确性submit_response{status:302,set_cookie_has_clearance:true,redirect_url:/}确认答案被接受clearance_stored{key:cf_clearance:a.com:abc123,ttl_sec:3600}确保存储成功clearance_expired{key:cf_clearance:a.com:abc123,reason:redis_ttl_expired}分析失效原因用 Winston Elasticsearch 实现Kibana 做看板。关键看板指标Challenge 成功率目标 ≥99.5%cf_clearance平均有效期应稳定在 55~65 分钟单次 challenge 平均耗时应 800ms实操心得有一次成功率突然跌到 80%日志显示大量compute_success但submit_response失败。查html_extracted发现s字段长度从 32 变成 48说明 Cloudflare 更新了 session token 生成逻辑。我们立刻从 Git 历史找回旧版 challenge.js同时通知计算层负责人更新算法。没有日志这次故障至少要花 4 小时定位。5.3 持续集成用 Git 管理 challenge.js 的版本演进challenge.js 不是静态资产它会随 Cloudflare 策略更新而变化。我的做法是为每个域名、每个jschl_vc值单独存一份 challenge.js并用 Git 管理其历史。目录结构/challenge-js/ ├── example.com/ │ ├── a1b2c3d4e5f67890.js # vc 值 │ ├── def4567890abcdef.js │ └── versions.json # 记录每个 vc 的生效时间、失效时间、是否弃用 └── another-site.com/ └── ...CI 流程Scheduler 每小时对重点域名发起探测请求。若返回 challenge提取jschl_vc和 HTML。计算jschl_vc的 SHA256检查该文件是否已存在。若不存在自动生成新文件提交 Git并触发 Slack 告警“发现新 challenge 版本已存入 /challenge-js/example.com/xxx.js”。计算层代码中solveChallenge函数根据jschl_vc自动加载对应文件。这样做的好处当某天cf_clearance大面积失效你不用大海捞针找原因直接git blame就能看到是哪个 commit 引入了新 JS然后对比新旧版本差异快速定位变更点比如新增了window.performance.memory检测。5.4 安全边界永远假设 challenge.js 是恶意代码最后也是最重要的一点绝不在生产环境用eval()或vm.run()执行未经审核的 challenge.js。Cloudflare 官方明确表示challenge JS 可能包含任意代码包括但不限于读取localStorage、sessionStorage虽无敏感数据但属越