
我最近在整理一份安全学习系列v2023的学习笔记第六十五讲正好落在JS应用安全漏洞利用这个主题上。说实话这一讲的内容跟我平时做前端代码审查时的场景非常吻合不需要等下一条注入请求而是从浏览器里那堆看似平常的JavaScript文件里把隐藏的接口、配置、鉴权逻辑和可控渲染点一个个翻出来。这篇笔记我会延续自己的整理思路先聊攻击面再拆解高频漏洞类型最后用一个本地模拟项目走一遍完整的前端漏洞利用与修复过程。1. 为什么JS应用安全值得单独记一讲1.1 前端JavaScript已经不只是“页面脚本”早年很多Web应用的JavaScript只负责表单校验、弹窗提示、简单轮播后端才是业务核心。这几年情况完全不同前后端分离几乎成了默认架构React、Vue这类框架把路由、状态管理、接口调用、用户会话全搬到了浏览器里。前端JavaScript实际上变成了一个“半公开的客户端程序”里面装着接口地址、权限判断逻辑、加密算法、甚至内部管理后台的入口。攻击者拿到的JS代码和被开发者写出来的几乎一致顶多是被压缩混淆过。很多人觉得压缩混淆能提高安全性实际上只能增加一点静态阅读成本代码逻辑还是要在浏览器里执行的。只要把压缩后的文件格式化再通过开发者工具动态调试逻辑迟早会浮现。更重要的是前端代码里经常能找到后端没有做二次校验的功能点比如把“是否是管理员”的判断放在localStorage里这种脆弱设计就是JS漏洞利用最典型的入口。1.2 这一讲适合谁需要哪些基础如果你做Web渗透测试只盯着Burp Suite里抓到的HTTP请求往往会漏掉很多前端逻辑漏洞。而这类漏洞用传统扫描器很难发现因为它们不一定产生独立的攻击请求而是通过修改浏览器内存对象、覆盖全局变量、改变前端的判断条件来实现。所以这一讲对做渗透测试的人非常实用。如果你是前端开发或者全栈开发看这讲的价值在于反向自检。很多前端安全问题不是不会修而是根本没意识到那行代码会成为问题。比如在模板里用v-html渲染用户评论比如把后端返回的某个配置对象直接挂到window上再比如为了方便调试把Token打印到控制台。这些操作在开发时“感觉没问题”但在安全视角下全是坑。基础要求不高懂基础的HTML和JavaScript语法能打开浏览器控制台理解HTTP请求和响应的基本流程就够了。2. JS应用攻击面梳理从点击按钮到请求发出2.1 源码与接口暴露前端代码是信息泄露重灾区一打开网站先看JS文件列表。现在大部分站点都会把代码打包成几个甚至几十个chunk文件很多业务模块是按路由懒加载的。攻击者的习惯不是逐个文件读而是先搜索关键词。我在实际测试中常用的搜索列表大概是api/、/v1/、/admin/、token、secret、appid、Authorization、password、upload、download。这些关键词能帮你在最短时间内定位接口前缀、认证方式、敏感配置。很多人以为把接口地址藏在某个子目录就安全但前端代码把所有地址都交代了只要在格式化后的代码里搜一遍就能找到。比接口地址更危险的是硬编码密钥。比如微信小程序AppSecret、地图SDK的Key、短信平台密钥甚至云存储的临时凭证。这类信息如果出现在前端JS里基本等同于公开。我见过不止一个项目把对象存储的AccessKey直接写在构建配置里结果整个存储桶都能被下载。这个问题在后端接口里可能看不出异常但在JS文件中一眼就能发现。2.2 请求封装与鉴权逻辑前端能不能直接决定谁登录前端现在普遍会封装一个请求模块统一处理URL前缀、请求头、Token注入、错误码。这个模块看起来只是基础设施但里面经常藏着安全缺陷。比较常见的是把错误码401自动清除localStorage并跳回登录页把403当成“无权限”并弹提示。听起来很合理问题在于判断权全在前端。攻击者并不依赖前端弹窗他会直接看请求逻辑找出哪些接口没有被请求封装拦截或者绕过封装直接发起请求。更直接的做法是打开控制台手动修改Token过期时间、手动把角色字段从user改成admin然后观察下一个请求是否带上了伪造的权限信息。如果后端接口没有对每个请求做独立的身份和权限校验那么前端无论怎么做防护都是白搭。所以我在测试中常做一个小实验登录普通账号在控制台里把本地存储里的角色改成管理员再看管理接口是否返回数据。如果返回了那就说明权限判断只发生在浏览器侧这个问题值得写进报告。2.3 路由、状态管理与第三方依赖身份判断藏在哪里前端路由也有类似问题。有的系统会在前端路由表里配置meta: { requiresAdmin: true }然后通过全局守卫判断用户角色。这种模式本身没问题但判断数据往往来自Vuex、Redux或本地存储这些数据在浏览器端都是可改的。利用方式很简单先用普通账号登录打开控制台修改状态管理里的角色字段然后调用router.push(/admin)如果页面能正常加载并请求到管理接口就是一个典型的前端越权漏洞。第三方依赖是另一个攻击面。很多项目会加载大量npm包、CDN脚本、统计脚本。供应链攻击一旦发生前端所有敏感信息都可能被窃取。虽然普通渗透测试不会真的去攻击第三方依赖但在梳理攻击面时要把外链脚本列出来看看它们是否具备全站DOM访问权限是否引用了可疑域名。如果站点存在CSP还要检查script-src是否允许了不安全域名。3. 高频漏洞类型拆解与利用链设计3.1 XSS的三大出口innerHTML、eval和动态加载XSS虽然老但依然是JS漏洞利用的重头戏。在新框架里直接写innerHTML的机会少了但总有例外。比如富文本编辑器、Markdown渲染、动态导入组件、模板字符串拼接。反射型和存储型XSS的关键是根据输出位置选择不同的编码方式。如果输入回显在script标签内部需要闭合标签如果回显在HTML属性里可能需要绕过引号过滤如果回显在事件属性里可以考虑图片标签的onerror。但前端框架通常会转义文本节点所以更值得关注的是DOM型XSS。DOM型XSS的典型链路是参数从location.search进入经过某个处理函数最终到达sink。常见sink包括document.write、element.innerHTML、eval、Function、setTimeout、setInterval、insertAdjacentHTML、outerHTML。比如某个搜索页把URL参数keyword直接塞进document.getElementById(result).innerHTML那就可以构造一个img srcx onerroralert(1)触发。在实际利用时还需要考虑浏览器自带的一些安全机制。现代浏览器对javascript:协议和某些XSS payload做了拦截所以单纯弹窗不一定能证明漏洞可利用。更务实的做法是尝试窃取数据用fetch把当前页面里的敏感信息发到攻击者服务器或者修改页面DOM诱导用户操作。但一定要注意所有测试只能在授权靶场或自己的实验环境里做。3.2 DOM clobbering用HTML标签覆盖全局变量DOM clobbering是我这几年越来越关注的一个点。它的原理很朴素浏览器会把带有id或name属性的元素自动挂到window或document上。也就是说如果页面里有一个div idconfig那么window.config就会指向这个DOM元素而不是原来声明的JavaScript对象。这种覆盖会导致什么假设业务代码这么写var config { admin: false, api: /api/user }; function showConfig() { if (config.admin) { loadAdminPanel(); } }如果攻击者能在页面中注入一个form idconfiginput nameadmin valuetrue/form那么window.config就被这个表单覆盖了config.admin会变成字符串true。由于非空字符串在JavaScript判断里为真loadAdminPanel()就会被执行。这个漏洞的巧妙之处在于它不一定需要XSS只要能往HTML里注入一个带id的元素就行。有些站点编程式创建DOM时会把某条用户数据拼进HTML片段数据里的标签名会受到一定限制但允许使用id属性这就给了DOM clobbering存活空间。我在测试时会重点检查代码里有没有像window.xxx、document.getElementById(xxx)之类的全局依赖。3.3 原型链污染一次JSON解析就可能让所有对象变成管理员原型链污染属于偏逻辑但破坏力很大的前端漏洞。核心原因是JavaScript对象的原型继承机制每个对象都有一个内部属性指向它的原型对象读取属性时会沿原型链向上查找。常见触发点集中在对象合并、深拷贝、配置覆盖等场景。如果代码里有一个递归merge函数把用户输入的JSON合并到某个目标对象上并且没有过滤__proto__和constructor.prototype就可能污染Object.prototype。例子function merge(target, source) { for (var key in source) { if (typeof source[key] object source[key] ! null) { merge(target[key], source[key]); } else { target[key] source[key]; } } }攻击者提交{__proto__:{isAdmin:true}}merge函数遍历到__proto__时会把它当成普通对象处理最终把isAdmin挂到所有对象共享的原型上。之后业务代码里只要判断某个用户对象的isAdmin属性因为读取不到自有属性就会沿着原型链找到true。前端原型链污染的影响范围取决于污染了哪个属性。如果污染Object.prototype.isAdmin可能直接影响前端权限如果污染Object.prototype.type可能改变上传逻辑里的文件类型判断。防御思路是在merge函数里排除__proto__、constructor和prototype这几个键同时使用Object.create(null)创建没有原型的纯净对象。3.4 前端加密与签名逻辑绕过不少系统为了“防篡改”会在前端对参数做MD5、AES、RSA加密或者拼一个签名串。这里有个很常见的误区前端加密防的是普通用户随意篡改但攻击者本来就能拿到前端代码。加密密钥和算法都摆在那里最多只是增加了一点时间成本。测试这类逻辑时我不需要从零逆算法。用浏览器开发者工具在加密函数处打断点观察入参和返回值然后直接在Console里调用暴露出来的加密方法生成一份合法的签名就可以了。更快的办法是把请求体复制出来用脚本语言重放并替换参数再看服务端是否校验参数的完整性。如果服务端只校验签名是否存在、不校验签名和数据的绑定关系那么前端加密逻辑就能绕过。给开发同学的修复建议也很简单任何敏感操作必须依赖服务端二次校验前端加密可以预防意外误操作但不能当作安全边界。真正需要防篡改的场景应该使用HTTPS配合服务端签名而不是把密钥埋在JS里。4. 实际操作一次模拟项目X的JS漏洞利用全过程4.1 信息收集控制台、网络面板和源码美化我搭了一个本地模拟项目X来演示这是一个前后端分离的跨平台管理系统有登录页、用户列表、订单查询和后台管理四个模块。正常登录后网络面板能看到一串JS文件app.js、vendor.js、api.js。这些文件已经压缩过第一行特别长直接当成文本读不现实。先把app.js复制出来用在线格式化工具或者IDE的格式化功能展开。格式化之后搜索关键词api能看到一个统一的请求前缀再搜索token会发现Token被存在localStorage。继续搜索admin在一个工具函数里看到function checkPermission() { if (window.userConfig.admin true) { renderAdminMenu(); } }这就是一个很典型的可被前端篡改的逻辑点。我决定继续跟踪window.userConfig在哪里赋值最后找到了启动时从/api/config拉取配置的代码。4.2 定位可控点从搜索参数到innerHTML在订单查询页发现页面会把URL参数keyword读取出来用于在结果区域显示“您搜索的是xxx”。开发者为了方便直接在HTML里拼了字符串var keyword new URLSearchParams(location.search).get(keyword); document.getElementById(search-result).innerHTML 您搜索的是 keyword;这个位置属于DOM XSS。正常思路是提交一个img srcx onerroralert(document.domain)验证弹窗。但我没有急着弹窗而是观察这个页面里有哪些全局对象可被利用。因为页面加载了userConfig并且在渲染菜单前会判断它。4.3 用DOM clobbering构造越权利用链我尝试在keyword参数里注入一个表单元素来覆盖userConfig。由于浏览器会把带id的元素挂到window上我构造了这样一段form iduserConfig input nameadmin valuetrue /form当innerHTML把这串内容插入DOM后window.userConfig就被这个表单覆盖了。原有的配置对象不再生效而表单里的admin输入框会被DOM clobbering规则映射成userConfig.admin值就是字符串true。在JavaScript里true本身不是true但在条件判断window.userConfig.admin true中用的是严格相等所以这里不会直接通过。不过如果业务代码用的是if (window.userConfig.admin)或者if (userConfig.admin true)字符串true会被转换为布尔值最终判断成立。模拟项目X的具体代码里用的恰好是宽松判断if (window.userConfig.admin true) { renderAdminMenu(); }所以注入成功后管理菜单被渲染出来页面也会请求后台管理接口。配合前面发现的接口路径一个普通用户就能看到本不该出现的后台功能。整条利用链不用改后端请求只是在URL参数里加了一段HTML片段就造成了前端越权。当然这个漏洞能成功的前提是页面存在XSS点、全局对象被覆盖、权限判断只在前端。如果服务端在管理接口上做了真实鉴权后端会拒绝普通用户访问DOM clobbering带来的影响就只是菜单显示异常不会扩大为越权。这也说明了为什么服务端权限校验永远不能省。4.4 修复建议与安全配置针对模拟项目X里的问题修复方案分两层。第一层是消除XSS不要把keyword直接拼进innerHTML应该使用textContent或者在框架模板里用{{ }}插值。如果确实需要渲染富文本建议用白名单过滤的DOM清理库而不是简单调用innerHTML。第二层是修正前端权限逻辑不能把userConfig挂到window更不能由前端决定菜单和接口的可见性。正确做法是每次请求都携带Token服务端根据会话解析用户角色接口层面做权限控制。前端菜单隐藏只是体验优化不是安全边界。额外可以做的是给页面加上CSP限制script-src为可信来源并配置trusted-types来拦截不安全的DOM操作。5. 常见问题与排查技巧实录5.1 压缩混淆后的JS怎么快速看压缩后的JS看着头疼其实有固定套路。第一步是格式化第二步是启动一个本地静态服务器用浏览器的开发者工具打开这个JS文件这样可以直接打断点不需要等线上环境触发。如果代码里加了混淆变量名比如_0x3f2a这种不要硬读。优先搜索字符串常量字符串通常不会被混淆成完全乱码。接口路径、错误提示、字段名都是很好的搜索切入点。比如搜api可能会跳到所有接口定义的位置搜password能看到登录相关的逻辑。高级的混淆器会把字符串也编码但这种情况相对少见遇到时可以在Console里手动调用解码函数或者借助浏览器环境自动解密。还有一点值得注意很多项目会在构建时生成Source Map文件如果线上保留了.map开发者工具里就能直接看到原始源码。虽然这不是一个漏洞但它会显著降低分析门槛在报告中可以建议线上环境禁用Source Map。5.2 外链脚本的漏洞为什么很难直接利用很多站点会加载第三方统计脚本、客服系统脚本、广告脚本。如果这些脚本本身存在漏洞攻击者理论上可以通过供应链方式影响站点。但在实际渗透测试中直接利用外链脚本漏洞通常很难因为第三方脚本的功能、输出位置都是不可控的。更实际的排查方向是查看外链脚本是否与页面有同源信任关系。有的站点会将第三方脚本的integrity属性省略也没有CSP约束一旦第三方域名被劫持或者脚本被篡改整个页面都会被操控。测试时我习惯把所有外链脚本的域名列出来检查是否和主站同源、是否使用了Subresource Integrity再决定是否把这个风险写进报告。5.3 我这几年总结的几个前端排查习惯第一个习惯是先从API配置入手而不是从页面功能入手。前端代码里所有接口路径和请求方式都会集中在某个模块里找到它等于拿到一份“接口地图”。第二个习惯是打开控制台前先看Network面板里的脚本加载顺序可以快速知道哪个文件是主业务、哪个文件是第三方依赖。第三个习惯是搜索本地存储操作凡是出现setItem、getItem、removeItem的地方都要想一想这些数据是否影响权限判断。调试时我喜欢在可疑函数上打断点刷新页面看调用栈。调用栈能告诉我这个函数是被谁触发的数据从哪个变量来最后又流向了哪个DOM操作。这套流程对定位DOM XSS和原型链污染都非常有效。5.4 利用失败时先检查这三件事有时候payload看上去没问题但就是不触发。先检查是不是HTML实体编码了如果后端或者框架把转义成lt;需要换一个不需要尖括号的payload。再检查是不是输出点被框架包裹在文本节点里这时候innerHTML不会执行但href、style等属性可能被利用。最后检查CSP如果页面限制了script-src内联事件和javascript:协议可能会被拦截。DOM clobbering利用失败也很常见。一个原因是现代前端框架会在编译时对模板中的表达式做更严格的求值比如Vue中直接访问userConfig会被当作组件属性不一定走window。另一个原因是某些浏览器对id和name挂载全局对象的规则有限制比如不是所有元素都会挂载或者已有变量优先级更高。遇到这种情况尝试改用a iduserConfig或者把name属性放到表单控件上再重新测试。我个人在实际操作中的体会是JS应用安全漏洞利用最考验的不是对某个payload的记忆而是对前端代码执行模型的理解。只要你能在脑子里模拟出浏览器解析、布局、执行脚本的完整过程很多看起来很隐蔽的漏洞都能变成一条清晰的利用链。踩过几次坑之后我也会把这部分内容再整理成一份自查清单以后写代码的时候把window全局变量、innerHTML、递归merge这些关键字当成敏感词来审查。这条路没有捷径多读真实的前端代码多做本地靶场实验比死记硬背漏洞库有用得多。