资讯动态

XSS攻防详解:从浏览器解析原理到通关实战与防御

发布时间:2026/10/22 11:02:41 来源:尧图企业网站定制
说起XSS恐怕再没有哪类漏洞的名声和真实危害差距这么大了。练习平台上第一关永远是弹个alert(1)导致很多人觉得XSS不就是让页面执行一下我写的JS么有什么好大惊小怪的可到了真实应用里XSS常年霸占高危漏洞榜前列原因只有一个它利用的是浏览器最底层的解析机制而几乎每个开发者都在写把用户数据拼进页面的代码。这篇内容写给两类读者一是刚接触Web安全、被反射型、存储型、DOM型几个术语绕晕的新手想系统搞清XSS到底是什么以及为什么要靠通关来练二是已经在刷XSS通关靶场、却总是背Payload、一换过滤规则就卡壳的进阶学习者。我会把XSS的原理讲透给出一套能贯穿绝大多数关卡的排查思路和绕过策略最后聊聊通关之后真正该掌握的防御姿势——毕竟能打进去只是第一步会防得住才算真正入了门。1. XSS的本质浏览器把数据当成了代码1.1 浏览器根本不区分开发者写的代码和用户提交的数据这是很多人对XSS最大的认知误区。你在浏览器里打开一个页面得到的是服务器已经拼好的HTML字符串。服务器端代码在生成页面时如果直接把用户输入拼接进HTML那么输入里的script、onerror这类内容到了浏览器这边就是HTML语法的一部分。浏览器不会回头看这个是开发者故意写的那个是用户填的它只负责按HTML规范把字符流解析成DOM、把脚本执行起来。可以这么类比你开了一间广播站来电话的人说什么你都直接对着麦克风播出去。有一天来电的人说我念到今天天气不错之后紧接着说一句现在所有听众起立你照单全收结果听众听到的不是天气而是你的广播被劫持了。XSS就是这种话被原样播出、还带上了执行效果的漏洞——只不过广播站变成浏览器说话的内容变成带HTML和JavaScript语法的字符串。所以判断一个地方有没有XSS不要只看有没有过滤特殊字符要看用户输入最终是否以可执行代码的形态到达了浏览器。过滤做得不好的站点可能防住了script却被img onerror打穿而有些看起来完全没过滤的页面如果输出点被放在纯文本节点里反而可能天然安全。关键永远在输出上下文。1.2 漏洞成立的四个条件缺一不可结合我做漏洞测试的经验一个XSS要成立通常要同时满足四个前提存在可控入口数据来自URL参数、表单字段、请求头、路由参数等至少有一个是攻击者能改的。数据进入了页面输出链路要么被服务器拼进响应页反射型和存储型的共同前提要么被前端脚本读走后写进页面DOM型的前提。输出环节没有做正确的上下文编码这是最核心的一条。说直白些开发者没有告诉浏览器这段是数据不是代码。输入最终落在可执行上下文比如HTML标签之间、标签属性值内部、JavaScript字符串内部、URL属性里。同一个字符串放在textarea里就是纯文本放在script字符串里就可能断句执行。把第三条和第四条放在一起看你就明白为什么很多号称过滤了XSS的代码依然会被打穿——它们把用户输入过滤一遍再用拼接的方式放进页面只要过滤规则有遗漏比如只处理了script却没处理onerror或者只处理了英文双引号却没处理反引号就很容易被绕过。后面讲通关套路时我会反复回到上下文这个词。1.3 名字里的跨站到底是什么意思很多人对跨站脚本这个译名很困惑我的Payload明明就注入在目标页面自身哪来的跨站这其实是历史遗留的叫法。早期的利用方式倾向于在一个站点里注入指向攻击者站点的脚本标签例如script src//攻击者域名/x.js/script让受害者浏览器去跨站加载恶意脚本于是得名Cross-Site Scripting。现在随着前端应用越来越复杂现代XSS早已不局限于跨站加载直接内联执行、DOM级利用都很常见但安全圈仍然沿用XSS这个缩写。搞懂这个名字还有个实际作用它会提醒你XSS的受害者是浏览器使用者而不是服务器。这决定了它的危害形态和修复思路都和其他漏洞很不一样——服务器端能做的只是尽量不让恶意代码进入页面真正被利用的环节发生在每个访问者的浏览器里。2. 反射型、存储型、DOM型三种XSS的判型方法论2.1 反射型XSS一次性利用链路在请求-响应之间反射型XSS是最容易理解的形态。攻击者把Payload放在请求的URL参数里服务器不做存储直接把它反射回响应页面。典型场景是搜索框/search?kw...服务器把关键词原样放进您搜索的内容是XXX的页面里不经过任何编码。比如一个极简的渲染逻辑!DOCTYPE html html body ?php echo 你搜索的关键词是 . $_GET[kw]; ? /body /html把URL构造成/search?kwscriptalert(1)/script响应里就会出现scriptalert(1)/script浏览器一解析就执行。这种漏洞的利用必须欺骗受害者点击攻击者构造好的链接所以通常出现在钓鱼链、短链接、社交工程等场景里。从排查角度来说看到搜索、翻页、排序、错误提示这类GET参数直接被拼进页面优先怀疑反射型。2.2 存储型XSS落库即持久危害按访问人次计算存储型和反射型的区别只有一个关键点数据是否被服务器持久化。评论区、用户昵称、个人签名、富文本编辑内容、上传文件名的展示都是存储型XSS的高发地。攻击者提交一次Payload之后每一个访问页面的人都会中招。在真实业务里存储型XSS是三类中危害最大的。原因很直接反射型还要诱导单个受害者点击链接存储型直接变成挂机自动攻击而且中招的往往还包括管理员等高权限用户。想象一下一个精心构造的帖子标题管理员在后台列表里鼠标悬停看了一眼标题里的onmouseover事件就执行了——如果执行的是修改管理员密码的接口调用整个后台就等于交了出去。存储型XSS的排查特征是入口和出口分离、且出口被很多人访问。代码审计时不能只看提交接口还得看所有渲染这个字段的地方。2.3 DOM型XSS服务器全程不知情DOM型XSS是最容易被新手忽略的一类因为它根本不经过服务器。漏洞源在页面自身的JavaScript某个前端脚本读取了可以被攻击者控制的数据源location.search、location.hash、document.referrer、localStorage等然后把它直接写进页面的可执行位置。一个非常典型的漏洞写法var name new URLSearchParams(location.search).get(name); document.getElementById(greeting).innerHTML 你好 name;访问https://example.com/?nameimg srcx onerroralert(1)URL参数被JS读取后通过innerHTML写入DOM图片加载失败触发onerror脚本执行。整个过程服务器只返回了一个毫无变化的页面所以你在服务器日志里根本看不出问题必须去读前端代码。这也是为什么前后端分离、页面大量使用前端渲染的项目里DOM型XSS的比例越来越高。排查DOM型需要关注的API包括innerHTML、outerHTML、document.write、eval、setAttribute、location相关的赋值等凡是外部数据 写DOM或执行代码的组合都要过一遍。2.4 一张表搞定判型与排查优先级类型触发链路是否入库利用条件危害规模排查重点反射型请求 → 服务器 → 响应否诱导受害者点击构造链接单个受害者/单次URL参数拼接点存储型请求 → 服务器 → 入库 → 响应是只需提交一次所有访问者持续触发所有渲染用户可控字段的位置DOM型请求 → 前端脚本 → DOM否只存在于浏览器侧诱导受害者访问构造URL单个受害者/单次但难排查前端JS中的敏感DOM写入API判型有一个非常实用的技巧先看输出点而不是输入点。在响应源码里搜索自己提交的测试字符串如果它出现在服务器返回的HTML里就是反射型或存储型再根据是否入库区分如果响应源码里根本没有你提交的字符串、页面里却弹窗了那几乎可以断定是DOM型——数据在中间绕了一圈才被前端脚本拾起来。3. 破坏力被严重低估XSS为什么常年排在高危风险前列3.1 从证明漏洞到实际利用alert(1)只是个起步练习平台上用alert(1)证明代码执行是因为它足够显眼、一眼就能确认但这也给新手造成了一个错觉XSS的利用就是弹个窗。真实的攻击载荷几乎不会用alert因为弹窗本身对攻击者没有收益。我习惯把攻击载荷分成几档来看信息窃取档读取当前页面的敏感内容、Cookie、表单输入、本地存储并通过网络请求发送到攻击者控制的服务器。行为模拟档在受害者浏览器上下文里以受害者的身份发请求改资料、下单、转账、发消息服务器根本分辨不出来。隐蔽劫持档注入键盘记录脚本、篡改页面显示内容、伪造登录框让用户主动把账号密码交出来。用一句话概括在浏览器眼里攻击者脚本和页面自己的脚本没有任何区别。页面能访问的攻击载荷就能访问用户能操作的攻击载荷就能替用户操作。3.2 窃取会话、页面钓鱼、键盘记录具体能做到什么程度先说最经典的Cookie窃取。如果Cookie没有设置HttpOnly标志页面里任何脚本都能通过document.cookie读到它然后发给远程服务器。即便设置了HttpOnly也只是挡住了直接读Cookie这一条路XSS脚本仍然能在受害者的会话里直接发请求、调用各种接口等于借用户的身份办事这比偷Cookie更难防御。还有一类利用是页面内嵌钓鱼。利用XSS直接在受害者正在访问的合法域名下注入一个虚假的登录框或中奖通知用户看到的地址栏是正确的、页面也是对的只是多出一段攻击者画的官方页面。这种依托于真实域名的钓鱼成功率比外部钓鱼链接高出一个量级。键盘记录也不难实现给document挂一个keydown监听器把每次按键内容收集起来回传。用户输入支付密码、账号、搜索敏感词全都变成攻击者的数据。这类脚本往往写得很小、很隐蔽普通用户根本察觉不到。3.3 从单一漏洞到连锁利用XSS如何引发系统性崩盘实战中最怕的不是单点XSS而是XSS作为跳板引发的连锁反应。举一个我在测试某后台管理系统时遇到的例子一个列表页的导出参数存在存储型XSS管理员登录后访问列表页就会触发。攻击载荷里先采集了当前页面的接口返回又利用管理员的会话调用了创建后台账号的接口几分钟之内攻击者就拿到了一个入口。这不是特例——后台管理系统功能全面、权限极高、前端却往往缺乏安全编码习惯这类系统里一个存储型XSS就等于整个后台的钥匙。所以安全社区把XSS列入Web应用最高危风险并不是危言耸听。它的输入面极其分散——搜索、昵称、评论、简历、富文本编辑器、上传文件名、URL参数几乎每个功能模块都可能是入口而它的危害又跟着受害者权限走——普通用户的XSS只能影响他自己管理员的XSS能影响整个平台。4. 通关的真正意义练习平台为什么都设计成关卡制4.1 关卡制的本质把XSS能力拆解成可重复训练的最小单元市面上的XSS练习平台无论公开在线还是本地自建几乎都采用关卡制这和编程刷题是同一个道理把XSS漏洞发现与利用这个宏大能力拆成一关一个知识点的粒度。第一关通常是一个完全没有过滤的简单输出点让你熟悉什么叫代码执行第二关开始引入关键字过滤第三关考验属性上下文再往后是各种编码叠加。每一关都是真实漏洞的某种简化投影。我挺建议学习者在刷这类平台时把每一关的考点记下来而不是只记通关Payload。因为这些考点在真实漏洞里反复出现script被过滤时要用事件处理器引号被过滤时要考虑无引号写法输出在JS字符串里要考虑断句闭合……你把这几个考点练熟了以后看真实代码时思路会快很多。4.2 关卡最常卡的三个维度过滤、上下文、编码刷过十几关之后你就会发现关卡设计绕不开三个维度。第一个维度是过滤规则。平台通常会在服务端或前端对输入做处理有的删除script关键字有的把尖括号转成实体有的替换引号有的干脆限制长度。你要做的是先判断它到底过滤了什么、怎么过滤的再针对性地绕过而不是上来就试一堆Payload。第二个维度是输出上下文。Payload放在HTML正文、放在value...属性里、放在script字符串里写法完全不同。这个维度决定了断句的姿势属性里要先闭合引号脚本字符串里要先闭合字符串和语句URL属性里可能直接使用javascript:协议。第三个维度是编码。HTML实体、URL编码、JavaScript的Unicode转义浏览器在不同阶段会做不同解码。很多看似双重过滤的关卡本质就是考察你对浏览器解码顺序的理解——某个字符被过滤层转成实体后浏览器又把它解码回来再交给下一个解析阶段。一关高难度的关卡往往是三个维度叠加先判断上下文再分析过滤最后考虑编码。这个分析链条就是通关的核心竞争力。4.3 通关的正确打开方式别背Payload练假设-验证循环我最常跟新人说的一句话是Payload库只能帮你过前三关后面的关卡靠的是方法。推荐的做法是养成一个假设-验证的循环先提交一个独特、容易识别的探针字符串比如zxssy目的是让它在响应源码里变得刺眼。打开开发者工具或直接查看响应源码找到这个字符串的落点观察它被放在什么标签里、引号和尖括号发生了哪些变化。基于观察结果形成假设这里的过滤器把尖括号转义了所以不能闭合标签但属性里的引号保留了……写一个针对这个假设的Payload提交验证不行就回退修正假设。这套循环看起来慢实际上才是职业渗透测试者的工作方式。拿真实业务系统也一样先测探针、看响应、定上下文、构造载荷。把刷关卡当成练这套循环比背一百个Payload有价值得多。5. 一套能打到中后期关卡的通用排查流程5.1 第一步先全局摸清数据从哪里进最终到哪里出拿到任何一道XSS题目我都不会立刻扔Payload。先做的事是把输入点从头到尾走一遍它在URL参数里还是在表单字段里提交之后请求到了服务器哪里响应页面中哪个位置出现了我的输入如果是前端渲染它又是被哪段脚本读走的为了把这条链路摸清楚最实用的手段就是探针字符串。找一个很独特的值比如xssprobe_abc把它填进输入点提交然后在响应源码里搜索xssprobe。看到它的那一刻你至少能回答三个问题第一我的输入原本样貌是什么样的尖括号和引号有没有被转义第二这个输入落在什么标签、什么属性、什么脚本块里第三服务器或前端在中间做了哪些处理。这三个问题基本就把漏洞的骨架勾勒出来了。这一步很多人会跳过觉得浪费时间但真实渗透里恰恰是这一步最值钱。你只有知道输出点在哪才知道后面所有Payload往哪里打。5.2 第二步判定输出上下文这是选Payload的唯一依据探针落点清楚了接下来给上下文归类。XSS的输出上下文大致分四类上下文特征典型Payload思路HTML标签内容输入夹在div.../div之间scriptalert(1)/script或img srcx onerroralert(1)HTML属性值输入在value...、title...等属性里scriptalert(1)/script或 onfocusalert(1) xJavaScript代码输入被拼进script、事件属性、onclick等;alert(1);//URL属性输入在href、src里javascript:alert(1)判断方法其实很朴素在响应源码里看探针被哪对标签或引号包裹着。被div包着就是HTML内容上下文被value包着就是属性上下文被var x 包着就是JS字符串上下文。判错了上下文Payload再精妙也不会执行因为浏览器根本不在那个语境里解析它。5.3 第三步用探针字符串摸清过滤规则判定上下文后还要知道过滤规则到底改动了什么。此时可以换一组更有针对性的探针把容易触发过滤的特征字符拆开测试 / \ script onerror img一次提交一个或一组看响应里哪个被删了、哪个被转义了、哪个原样保留。这些观察结果直接决定你后面Payload的写法比如尖括号被编码成lt;和gt;HTML内容上下文这条路基本断了得考虑属性上下文或者找有没有其他未过滤的输出点。script关键字被删除但尖括号原样保留说明是黑名单拦截可以试试img、svg、iframe等标签或者用scrscriptipt让过滤后的字符串重新拼接成合法的script。引号被过滤或编码但尖括号保留Payload里尽量不用引号事件属性里可以写成onerroralert(1)JS上下文可以用反引号或String.fromCharCode拼字符。空格被过滤用/、制表符、换行、注释符/**/代替HTML属性里的分隔。这一步做完过滤规则的画像基本就出来了。别再奢望猜中唯一正确的Payload先搞清楚规则正确的Payload就是规则下的自然推论。5.4 第四步按上下文选载荷按过滤做变形上下文和过滤规则都清楚了最后一步才是选载荷。组合逻辑一句话就能说清先按上下文选基础形态再按过滤规则做变形。举个例子。HTML正文上下文、无过滤直接用scriptalert(1)/script。但如果发现script关键字被删基础形态就要换成事件触发型img srcx onerroralert(1)因为img标签不在黑名单里onerror也还在。如果连onerror也被删那就继续换思路svg/onloadalert(1)、details open ontogglealert(1)或者用onerror的编码变体——关键在于你始终在上下文允许这个标签或事件的语法和过滤规则不知道这个变体的交集里找路。变形的手法也有套路可循大小写混淆、关键字中间插入注释或标签、HTML实体编码、Unicode转义、字符拼接、利用未被过滤的等价标签。多练几关之后你会发现所有变形都围绕着同一个目标让过滤后的字符串在浏览器解析时重新变成看起来合法的那一段。6. 五个常见关卡形态的实战拆解这一节我用几个虚构但非常典型的关卡形态把上面的流程从头到尾走一遍。这些形态不绑定任何真实平台但你在任何一套XSS练习环境里都会遇到同类设计。6.1 关卡形态A输入点裸落在HTML正文中场景很简单一个文章标题搜索功能div classresult你搜索的内容是!-- 你的输入 --/div提交探针xssprobe_abc看响应源码尖括号原样保留没有过滤上下文是HTML正文。那闭着眼都能过输入本身就是标签语法。Payloadscriptalert(1)/script或者更稳妥地用不依赖script标签的写法img srcx onerroralert(1)前者胜在直观后者胜在即便script被部分过滤也能跑通所以我在真实测试里习惯优先试事件型Payload。记住这一关的考点是认识HTML正文上下文后面的关卡都是在它之上加过滤条件。6.2 关卡形态Bscript标签被过滤改用事件处理器同样的输出位置这次服务端做了处理把大小写不敏感的script关键字全部删除。提交scriptalert(1)/script响应源码里变成alert(1)说明关键字被吃掉了但尖括号还在。这时候别再纠结怎么把script拼回来虽然scrscriptipt也能过但更简洁的思路是换标签。HTML正文上下文里能执行JS的标签远不止script一个img srcx onerroralert(1)浏览器加载图片失败触发onerror事件执行alert(1)。这个Payload里没有任何script关键字过滤规则形同虚设。还可以换成svg onloadalert(1) iframe srcjavascript:alert(1)这类标签加事件的组合在真实漏洞里比script出现频率更高因为开发者做黑名单时第一反应就是拦script能想到拦onerror、onload这些事件的反而少。6.3 关卡形态C输入点被包在HTML属性值里场景变成input typetext value!-- 你的输入 --这次探针显示输入被包在value...里属于属性上下文。此时直接提交scriptalert(1)/script是无效的——它只会被当作value属性的普通字符串的一部分尖括号在属性值里没有开辟新上下文的能力除非你先把属性值闭合掉。所以这一关的核心动作是断句闭合。Payload autofocus onfocusalert(1) x渲染后整个标签变成input typetext value autofocus onfocusalert(1) x第一个双引号闭合了valueautofocus让输入框自动聚焦onfocus在聚焦时执行恶意代码最后的x是为了避免语法混乱而补的一个无意义属性。对比用scriptalert(1)/script这种写法也能过但事件型通常更短、更少依赖script标签。属性上下文的判断信号很明确探针前后是引号且引号没有被编码。如果引号被转义成quot;那断句这条路就被堵上了得考虑别的上下文或别的输出点。6.4 关卡形态D输入点被拼接进JavaScript代码场景再升级输出点变成scriptvar keyword !-- 你的输入 --;/script输入被拼进一个JS字符串里这是非常经典的脚本上下文。此时提交img srcx onerroralert(1)不会执行——它在JS字符串里只是一段普通文本因为没有语法让它脱离字符串。正确思路是闭合字符串闭合语句注释掉尾巴。Payload;alert(1);//渲染后变成var keyword ;alert(1);//;解析顺序闭合了原本的字符串开头;结束当前语句alert(1)成为新的语句执行//把后面残留的;注释掉避免语法错误。这一关的考点是JS字符串的引号闭合和注释技巧在真实代码里任何把用户输入拼进script或事件属性的写法都是这个套路。6.5 关卡形态E多重过滤叠加考验编码理解最后一种综合形态输入点在HTML正文过滤器把script关键字删除把onerror删除还把英文引号编码了。看起来叠了三层规则很多新手到这里开始堆Payload猜答案。按前面的流程走探针先上确认三件事——尖括号原样保留、onerror被吃、引号变实体。上下文是HTML正文但常规的img onerror路线被堵。此时想想还有没有别的标签事件组合能绕过关键字拦截onload还在svg标签也还在。Payloadsvg onloadalert(1)svg是合法标签onload不在黑名单里事件值里一个引号都没用正好绕开三层过滤。渲染后浏览器载入SVGonload触发alert(1)执行。这个例子很好地说明了为什么我更看重分析流程而非Payload收藏分析得出的结论是黑名单漏了onload和svg顺着这个缺口走比在onerror上死磕高效得多。7. 从通关切换到防御攻防视角的最后一课7.1 输出编码按上下文编码才是唯一可靠的防线刷完关卡如果只记住一件事我希望是XSS的根治方案在输出端而且是按上下文编码不是防一下特殊字符。服务器在把用户数据放进页面时必须针对它将要落进的位置选择编码方式输出上下文编码方式编码示例HTML正文HTML实体编码转lt;转gt;转amp;属性值双引号内HTML实体编码 引号转义转quot;JavaScript字符串JS转义转\x27转\x3CURL属性URL编码整体encodeURIComponent很多人写了多年业务代码输出编码只会做一个 htmlspecialchars遇到属性上下文、JS上下文照样被打穿原因就是没有按上下文选编码。这也是为什么现代主流前端框架默认对插值做转义的原因——它们帮你把上下文编码这件事自动化了。但如果你用innerHTML、v-html、dangerouslySetInnerHTML这类强制插入HTML的接口等于亲手关掉了框架的自动防护这时候所有责任又回到开发者身上。7.2 输入校验与纵深防御白名单、CSP、HttpOnly的正确理解输出编码之外还有几层纵深加固。输入校验只应该做业务合法性的事比如年龄字段必须是数字、手机号必须匹配格式这类严格格式的字段用白名单校验即可但绝不要指望输入校验能拦XSS因为HTML和JS的表达方式太多黑名单根本列不完。CSP内容安全策略是真正有效的第二道防线。给站点配置script-src只允许从白名单域名加载脚本禁止内联脚本即使输出端漏了一个XSS被注入的script或事件内联脚本也会被浏览器拦下。当然CSP也有使用成本老项目里大量内联脚本会让策略配置变得很痛苦需要逐步调整。还有一点必须澄清HttpOnly Cookie被很多人误当成XSS的解药它其实只是给攻击者偷Cookie增加了障碍。前面说过XSS脚本根本不需要偷Cookie它可以直接借用户会话发请求。所以HttpOnly是值得做的加固项但永远不能替代输出编码。7.3 为什么过滤脚本思路注定失败回头看那些被XSS打穿的系统几乎都有一个共同点安全措施停留在过滤输入层面——过滤关键字、过滤标签、过滤事件名。这种思路的致命弱点是过滤规则永远滞后于浏览器语法。浏览器支持的标签有上百种、事件处理器有上百种、编码组合更是无穷无尽你拦住了onerror攻击者换onload你拦住了onload攻击者再换onmouseover加autofocus你封了标签白名单攻击者还会利用已有的合法标签配合奇特的属性组合。安全的正确姿态是默认输入不可信不信任任何用户可控数据输出时统统按上下文编码。这样无论攻击者提交什么花哨的Payload到了页面里它都只是被编码过的普通文本浏览器永远没有机会把它当代码执行。这也是为什么很多资深安全工程师说不要问这个输入能不能过滤要问这个输出有没有编码。最后分享一点我个人的体会。我带新人刷XSS练习平台时最明显的分水岭不是谁背的Payload多而是谁先养成看响应源码、定上下文、测过滤规则的流程。一开始可能慢但十关二十关之后这个流程会内化成一种直觉看到一段代码就能猜到它的输出点在哪、大概会卡在哪类过滤上。练XSS本质上练的是对浏览器解析机制的理解机制吃透了无论以后碰到练习平台还是真实系统的代码审计你都能用同一套思路解决问题。也提醒一句所有测试请在自己的授权靶场或自建环境里进行不要把探索的脚步伸向不属于你的系统——技术是用来理解世界而不是破坏世界的。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑