SPA扫描攻防:从JavaScript渲染到无头浏览器路由挖洞实战

发布时间:2026/9/10 9:29:29
SPA扫描攻防:从JavaScript渲染到无头浏览器路由挖洞实战 第一次拿AWVS去扫一个Vue写的单页应用时报告干净得让我怀疑人生。登录页、几个静态页面全部“无漏洞”但越是这样我越不敢信。后来换成Playwright把页面真正渲染出来再旁路抓一遍流量光API接口就漏出来几十个里面还躺着两个越权。从那之后我就一直跟团队强调SPA的扫描第一步根本不是“扫”而是先把页面还给浏览器让它把JavaScript跑起来、把路由走一遍否则你扫的只是一个空壳。这篇就围绕SPA扫描攻防展开从JavaScript渲染机制、hash/history两种路由模式给传统扫描器带来的坑到无头浏览器方案、JS bundle路由逆向、自动化流水线落地再到实际扫描中最容易翻车的几个场景。既适合刚入门安全测试的开发者也适合正在为“扫不到东西”发愁的安全工程师。1. SPA为什么让传统扫描器集体“失明”1.1 渲染机制浏览器才是真正的“页面”传统网站是服务端渲染访问一个URL服务器返回完整HTML内容和结构都在里面爬虫直接解析文档就能拿到所有链接和表单。但SPA完全不是这个套路它初始返回的HTML通常就是一个空壳里面只有div idapp/div和一个script标签剩下的所有东西都是JavaScript在浏览器运行时动态生成、填充进去的。这就产生了一个致命问题传统扫描器的爬虫模块根本不执行JavaScript它拿到的是那个只有几行代码的空壳HTML看到的自然只有根节点和登录页。页面真实内容、路由指向的页面结构、挂载在页面上的所有请求全都藏在浏览器运行时的内存里以DOM和网络请求的形式存在。所以要让扫描器看到真实面目就必须有一个能完整执行JS的浏览器环境。这也是整个SPA扫描攻防里最核心的认知转变你得先让页面“活”起来再去谈挖洞。1.2 路由模式扫描器的盲区重灾区SPA的路由有hash模式和history模式两种。hash模式的URL形如http://target.com/#/dashboard路由信息在#之后。问题在于浏览器发HTTP请求时压根不会把#后面的部分传给服务器服务端日志里永远是GET /传统扫描器也只会记录到/这一个URL所有hash路由页面在扫描器眼里完全不存在。history模式看起来友好一些URL是http://target.com/dashboard这种“正常”的样子但服务端必须把所有路径都fallback到index.html否则直接访问会404。扫描器虽然能记录到/dashboard这个URL可如果它直接对/dashboard发请求服务器返回的是又一个空壳HTML依然等于什么都没扫到。更麻烦的是SPA前端路由和后端真实接口是两回事。前端路由决定页面长什么样接口才决定数据从哪里取。前者是/user/123后者可能是/api/v1/users/123。如果不把前端路由跑起来、不捕获浏览器发出的真实API请求你永远不知道自己真正该测的资产是什么。这也是为什么SPA扫描一定要把“路由走一遍”和“流量抓一遍”这两件事同时做。1.3 三个我实际遇到的失效场景第一个场景目标是个React后台管理系统用Burp被动扫描挂了半小时sitemap里只有登录页和一个静态about页后来用无头浏览器跑完所有路由抓到了60多个API请求很多还带ID参数越权测试立刻就有了抓手。第二个场景一个Vue项目用了懒加载和动态导入核心功能模块是用户登录后点击菜单才加载对应JS chunk的这些模块里的接口请求根本不会在首次渲染时出现。传统扫描器就算能执行少量JS也拿不到只在特定交互后才发出的请求。第三个场景目标SPA用hash路由所有后台功能地址都是/#/system/user这种形式。我让自动化脚本把所有#/开头的路径全部提取出来转换成可测试URL再用无头浏览器逐个访问才发现系统逻辑漏洞远比首页看到的复杂得多。所以扫描SPA的第一原则永远不要相信静态HTML里的世界那只是冰山一角。2. 抓取SPA渲染内容的工具路线与选型2.1 无头浏览器最直观也最“重”的方案无头浏览器就是没有界面的浏览器但它内核完整能执行JS、渲染DOM、发网络请求。主流的方案是Playwright、Puppeteer、Selenium。我的建议是优先Playwright理由很直接API设计清晰支持同步和异步两种模式内置webdriver兼容wait逻辑比Selenium好用太多而且它对SPA的SPA路由变化监听处理得更好。无头浏览器方案的核心价值不只是渲染DOM它还能在渲染过程中旁路抓取网络请求。你只需要在浏览器上挂一个response事件监听器把每个响应的URL、状态码、请求方法记录下来就能在“用户真实操作”的过程中顺带完成资产收集。这比从静态JS里正则抠接口要完整得多。缺点是资源消耗大一个浏览器实例动辄一两百MB内存并发几十个页面就需要一台配置还行的机器。所以无头浏览器适合“精扫”而不是“海扫”它用来做种子页面渲染和API捕获真正的高并发测试交给其他工具。2.2 预渲染与爬虫框架轻量一点的替代路线如果你不想维护一堆浏览器实例还有一种思路预渲染。它的原理是预先用无头浏览器把SPA页面渲染成静态HTML然后缓存下来后续扫描请求直接命中缓存拿到完整DOM。常见实现是prerender服务或者是前端框架自带的SSR/SSG方案。这个办法在目标SPA页面数量不多、且路由列表已知时效果不错等于一次性把“空壳”变成“实心”。另一个方向是用上层爬虫框架比如Python里的Scrapy加Selenium中间件或者Node生态的Crawlee。爬虫框架负责调度、去重、重试无头浏览器负责渲染两者结合能省不少事。我实测下来Scrapy加Selenium中间件在小规模目标时可用但一旦路由数量上千队列管理和去重逻辑就会变成瓶颈。Crawlee做得更现代一些对单页应用的支持更好默认带请求队列和哈希去重适合写自动化流水线。2.3 我的常用选型组合做了这么多项目我现在的默认组合是Playwright写渲染和采集脚本负责搞定页面执行和流量捕获Nuclei或者自写Python脚本做接口级批量探测负责把采集到的API列表送进去跑权限和注入类检测如果遇到单一页面复杂度极高的目标比如嵌套路由加大量动态参数再加一层Burp Suite把无头浏览器配置成代理流量全部转发进Burp里做被动扫描。选型的核心思路是渲染和执行能力交给浏览器级别的工具批量探测和规则匹配交给专门的扫描器人只做判断和决策。不要指望一个工具解决所有问题SPA扫描尤其如此。3. 高级路由场景路由表提取与参数挖掘3.1 先判断路由模式再决定策略动手之前先花两分钟判断目标用的是哪种路由模式。方法很简单打开页面看URL里有没有#或者访问一个不存在的深层路径比如/some-random-path如果服务器返回的还是index.html的内容状态码200基本就是history模式并配置了fallback如果返回404那要么是未配置要么是hash模式。路由模式会直接决定你的策略。hash模式下你完全不需要关心服务器路径只需要把所有#/xxx提取出来在浏览器里逐个执行history模式下你要关心的则是那些真实映射到前端路由的路径同时还要确认服务端fallback是否配好否则无头浏览器直接访问深层路由也可能拿不到页面。判断错误会导致后续所有路由遍历都白做。3.2 从JS bundle中逆向提取路由表拿不到源代码时JS bundle就是最好的路书。Vue Router和React Router的路由配置几乎都会把path字段以字符串形式留在打包产物里。所以只需要下载所有的JS文件用正则把path: xxx和name: xxx匹配出来再过滤出以/开头的路径基本就能拼出一张完整的前端路由表。下面是一个快速提取脚本适用于大部分Vue/React项目import re import json app_js open(rdist/assets/app.1a2b3c4d.js, r, encodingutf-8).read() paths set() for m in re.finditer(rpath\s*:\s*[]([^])[], app_js): path m.group(1) if path.startswith(/) and len(path) 200: paths.add(path) print(提取到路由数量:, len(paths)) for p in sorted(paths): print(p)从项目实际经验来看这个正则能匹配到80%以上的静态路由但动态路由还需要额外处理比如path: /user/:id这种。:id这类参数名本身就带有语义比如id、uid、page、tab后面做参数枚举时可以直接拿来当字典起点。如果bundle做过高度混淆正则匹配失效那就退回到无头浏览器运行时Hook劫持window.history.pushState和window.location.hash的赋值操作在用户或自动化脚本跳转路由时记录URL这个方案对混淆代码同样有效。3.3 动态路由参数识别、取值与批量枚举前端路由里的动态参数后端接口里大概率也有对应关系。前端是/article/:id后端往往就是/api/v1/article/123。所以当你从前端路由表里发现动态参数后下一步就是猜参数对应的真实API然后批量枚举。我的做法是三步走。第一步先把路由参数名整理成字典比如id、uid、userId、page、cateId第二步用无头浏览器访问带具体值的路由比如访问/article/1和/article/2观察页面渲染内容和网络请求日志找到真实的API路径和参数名第三步拿到API模板后用Nuclei或httpx批量替换参数值注意要把ID类的小数字段和控制页面逻辑的字段分开处理前者做越权枚举后者做逻辑漏洞探测。一个小技巧SPA页面通常会在渲染列表时一次性请求多个资源所以抓网络日志时别只看业务API静态资源里的配置接口、文件上传接口、导出接口往往藏在这些动态页面里很多低危中危漏洞就是这么捞出来的。3.4 路由守卫带来的鉴权判断价值Vue Router的beforeEach、React Router的Navigate组件和路由守卫是判断前端权限逻辑的重要线索。很多开发者会在前端做一层“防君子不防小人”的登录跳转未登录访问/dashboard时自动redirect到/login。这个逻辑对普通用户有效但对测试者来说恰好可以用它来推断哪些路由是需要登录才能访问的后台功能。你甚至可以故意不带token去批量访问路由表中的所有路径统计哪些路径会跳转到登录页、哪些路径直接暴露了页面内容。如果某个需要认证的页面没有跳转直接返回了数据那就是一个典型的前端鉴权缺失或API越权。测试时把token从localStorage里取出来注入到请求头再访问一遍如果返回数据比未授权时更完整至少说明前端权限控制和后端校验是脱节的值得深挖。4. 自动化扫描流水线的落地实现4.1 五步流水线的整体设计我常用的SPA自动化扫描流水线分五步第一种子URL巡检无头浏览器打开首页通过路由表和常见菜单入口收集初始URL集合第二全量渲染对每个URL执行页面渲染并等待网络空闲第三步采集数据包括渲染后的HTML、网络请求日志、localStorage和cookie状态第四步数据去重和规范化过滤静态资源、规范URL格式、去重API路径第五步批量探测把清洗后的API路径喂给扫描器跑权限、注入、未授权访问等检测。每一步都有独立的脚本用一份JSON在中间传递数据方便随时断点续跑和人工介入。4.2 无头浏览器渲染并旁路抓包下面是一个可用的Playwright脚本完成“打开页面、等待渲染、捕获API请求”三个动作import json from playwright.sync_api import sync_playwright api_log set() def on_response(response): url response.url if any(x in url for x in [/api/, /v1/, /v2/, .json]): api_log.add(f{response.request.method} {url} {response.status}) with sync_playwright() as p: browser p.chromium.launch() page browser.new_page() page.on(response, on_response) page.goto(http://target.com/#/dashboard, timeout30000) page.wait_for_timeout(5000) # 等待异步请求结束 browser.close() for line in sorted(api_log): print(line)这段代码的核心是page.on(response, on_response)事件监听。SPA最大的特征是页面URL没变、但网络请求一直在发生所以不能只靠URL变化来触发采集必须监听网络层。wait_for_timeout(5000)是为了等懒加载模块和异步请求跑完具体等待时长根据目标响应速度调整网络慢的时候可以改成轮询networkidle状态。4.3 把数据喂给扫描器的两种姿势采集完成后的数据是一份API列表。我一般会做两个处理。第一用httpx写一个简单的批量请求脚本默认带cookie或token做一次“直连探测”目的是确认哪些API在无页面上下文时候能直接访问这一步能快速筛出未授权接口第二把API列表转成Nuclei模板的请求目标或者直接导入到像Burp的Intruder里做参数级测试。Nuclei的template可以写成这样id: spa-api-enum info: name: SPA API Endpoint Probe severity: info http: - method: GET path: - {{BaseURL}}/api/v1/users - {{BaseURL}}/api/v1/orders stop-at-first-match: false matchers: - type: status status: - 200 - 403不需要复杂的匹配规则先确认哪些路径存在再针对有响应的路径做细粒度测试。我见过很多人在这一步倒过来想把所有漏洞都塞进一个模板里结果模板臃肿、误报一堆反而降低效率。SPA自动化扫描的精髓是“先广后深”存在性确认永远在先。4.4 资源控制与稳定性细节自动化跑SPA扫描最容易翻车的就是资源控制和稳定性。无头浏览器跑久了会内存泄漏页面开太多会直接把机器拖死。我个人的经验是并发数控制在5到10个页面实例每个页面用完立即关闭用page.wait_for_load_state(networkidle)代替固定sleep来提升速度给每个页面加30秒超时超时直接杀掉宁可漏一个页面也不能让整个任务卡死。还有一点容易被忽略反爬和风控。有些SPA页面会检测webdriver特征检测到就不渲染核心模块。遇到这种情况可以给Playwright加args[--disable-blink-featuresAutomationControlled]或者用stealth插件做基础伪装。不过要注意安全测试是在授权范围内进行的伪装只是为了完成授权的测试任务不要越过边界去做其他事情。5. 实战避坑扫描失败案例与排查手册5.1 页面白屏或渲染不完整现象无头浏览器打开页面后截图是白的或者内容只有一半。排查步骤第一打开浏览器控制台看有没有红色报错最常见的报错是跨域问题或者某个JS资源加载失败第二检查是不是SPA部署在子路径下比如http://target.com/admin/打包时的publicPath没有配置导致JS请求路径全是/static/xx.js直接404第三看是否是浏览器版本太老不支持目标项目用到的新语法。处理建议优先在脚本里捕获console日志和pageerror事件把错误信息写入文件方便回查。白屏不解决后面所有采集都是空的这一步不能跳过去。5.2 路由跳转导致URL丢失现象无头浏览器通过点击事件触发路由跳转但采集到的还是最初的URL。原因SPA跳转用的是pushState或hash变更不会产生新的HTTP请求如果只依赖监听URL变化来触发采集那跳到哪一步都捕捉不到。处理建议改用两层监听。第一层监听response事件抓网络请求这是最可靠的第二层用脚本定期读取当前URL和上一次记录做比对有变化就触发一次DOM和路由状态采集。5.3 登录态失效导致全站回退现象采集刚开始还正常跑了一百多个页面后所有响应都变成跳转登录页。原因SPA普遍用token做认证token放在localStorage或cookie里过期后全站自动回退到登录页。自动化脚本不会自动重新登录后面的页面全是登录页渲染结果白跑。处理建议测试前把目标token的过期时间摸清楚。如果是测试环境请开发把token过期时间调很长如果无法调就在脚本里做响应拦截发现跳转登录页时自动回到登录接口重新拿token并恢复挂载到localStorage里。这个逻辑不复杂但能省掉大量重复劳动。5.4 WAF/风控拦截与误报现象本地反复测试没问题的页面一放到批量扫描阶段就超时、500、或者返回验证码页面。原因自动化请求速度太快触发目标WAF或风控策略。SPA本身请求就密集再加上扫描器连发很容易被误判为攻击流量。处理建议控制扫描速率每请求间隔1到3秒打乱请求顺序不要按字典序一路扫下去对响应内容做模糊匹配如果返回了验证码或“访问过于频繁”之类的内容立即暂停任务而不是继续硬扫。5.5 JavaScript框架漏洞误报的真伪判断现象扫描器报了一个“检测到目标站点存在JavaScript框架库漏洞”比如某个版本的Vue或jQuery存在已知CVE。这类报告需要人去判断。很多SPA项目虽然引用了存在漏洞的框架版本但实际开发中已经对危险函数做了封装和过滤漏洞根本不可利用。不要直接当作高危上报也不要直接忽略。我的处理方法是先确认框架版本号是否真实存在漏洞再检查漏洞函数是否暴露在可访问接口中最后验证业务代码是否对输入做了限制。三步确认完再定性。SPA扫描里误报率最高的往往不是漏洞本身而是这种“依赖库级”的报告。另外还有一种典型误报来自静态扫描工具比如直接把整个JS bundle丢给Fortify或类似工具做静态分析会报出海量前端代码缺陷但其中大部分在运行时不可达。我的原则是静态扫描结果只能用来辅助定位最终判定必须结合动态验证。结尾做SPA扫描攻防这么久我最大的感受是扫描器只是工具真正决定产出质量的是你能不能理解SPA的渲染和路由机制。传统站点扫的是URLSPA扫的是“渲染后的页面运行时流量”整条链路里的每一步——无头浏览器选型、路由表提取、参数枚举、防误报判断——都是在为这一个目标服务。最后分享一个小技巧所有SPA自动化采集到的URL和接口我都会按“路由类型、后端API、参数特征、认证状态”四个字段存成表格。做完一个项目这张表格本身就是一份资产清单下次再遇到同类前端框架堆栈的目标直接拿来当字典用能省至少三分之一的时间。扫描攻防的核心从来不是工具多高级而是你有没有真正把目标的结构看透。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询