XSS跨站脚本
XSS跨站脚本
复习定位
XSS(跨站脚本)是Web安全中最广泛存在的漏洞。攻击者向网页中注入恶意JavaScript代码——当其他用户浏览该页面时——恶意代码在用户的浏览器中执行——可以窃取Cookie、篡改页面内容、发动钓鱼攻击等。XSS根本原因:网站将用户输入的数据未经过滤地直接输出到页面上——用户的输入被当成了浏览器可执行的脚本。防御要点:输出编码——把用户输入中的HTML特殊字符转义为实体。
XSS的类型与攻击方式
反射型XSS(Non-Persistent XSS)——恶意脚本隐藏在URL的请求参数中——服务器直接从请求参数取值嵌入到响应HTML中未做编码。用户点击一个攻击者构造的恶意链接——链接中包含<script>alert(document.cookie)</script>——服务器搜索页将参数直接输出——浏览器解析响应时执行了恶意脚本。反射型XSS一般需要用户点击特殊构造的链接(如钓鱼——邮件附件链接等)。
存储型XSS(Persistent XSS)——攻击者将恶意脚本存储到目标服务器的数据库(如论坛评论、用户昵称)——任何用户浏览到该页面时恶意脚本自动执行。不需要用户点击特定链接——所以危害极大——一个知名社交平台的XSS可以让攻击者在所有浏览该网页的用户浏览器中执行任意操作。
DOM型XSS——攻击脚本完全在客户端运行——不经过服务器。代码如document.getElementById("output").innerHTML = location.hash——攻击者在URL的#后注入<img src=x onerror=alert(1)>——客户端JavaScript将数据直接赋值给innerHTML——脚本执行。
防御
输出编码(HTML Entity Encode)——将用户输入中的< > & " '转换为< > & " '——使浏览器将其显示为普通文本而不是标签代码。
CSP(Content Security Policy)——通过HTTP响应头Content-Security-Policy: script-src 'self'——告诉浏览器只允许加载和执行来自同一来源(同协议域名端口)的JavaScript——阻止了任何内联脚本和外部第三方脚本被加载执行——即使攻击者注入的<script>标签——浏览器也拒绝加载它。
HttpOnly Cookie——设置cookie为HttpOnly后——JavaScript的document.cookie无法读取该cookie——以此阻止XSS攻击直接窃取用户session ID——但仍要防范其他攻击方式(如XSS构造误导表单等)。
非HTML内容的XSS
不只是<script>标记内可以执行代码——事件处理器属性如onerror=alert(1)、javascript:伪协议在<a href>中也可能被利用。所以需要对所有出现在HTML不同上下文(HTML标签体/属性/URL/CSS/JavaScript字符串)中的用户数据进行上下文相关的编码。
复习检查
反射型XSS和存储型XSS的根本区别——攻击脚本的存放位置分别在URL请求参数还是在服务器数据库——哪一种的攻击范围更大?
为什么仅仅对输入的尖括号
<>转义不足以预防所有XSS——onerror事件属性和javascript:伪协议在HTML属性值中也会造成攻击——防御时需要转义什么特殊字符?CSP的
script-src 'self'不能阻止攻击者通过本域资源库(如JSONP回调)中的可执行脚本来绕过——补充strict-dynamic等新CSP指令如何提升安全性?HttpOnly Cookie不能让XSS忽略所有"cookie被盗"的风险——但攻击者仍然可以发起CSRF攻击(不读取Cookie——直接发送身份认证请求)。为什么XSS攻击者在获取不了
HttpOnlycookie之后——仍然能以该用户身份进行部分操作?为什么许多网站使用
DOMPurify库来清理用户输入的HTML——简单替换<>` 等字符为何在富文本编辑器中不适用——且必须允许用户加入一些样式和标签内容?
XSS的三大分类详解(扩展)
反射型XSS(非持久性)——恶意脚本隐藏在URL的请求参数中——服务器直接从请求参数取值嵌入到响应HTML中未做编码。用户点击一个攻击者构造的恶意链接——链接中包含<script>alert(document.cookie)</script>——服务器搜索页将参数直接输出——浏览器解析响应时执行了恶意脚本。反射型XSS一般需要用户点击特殊构造的链接(如钓鱼——邮件附件链接等)。
反射型XSS的危害虽然单次需要用户交互——但在大型社交平台上可以通过组合技巧放大——例如攻击者构造一个恶意链接伪装成正常内容——通过消息/邮件发送给大量用户——由于URL中的恶意代码在用户不知情的情况下执行——并可以利用AJAX调用在后台持续转帖从而达到大面积感染的目的。
存储型XSS(持久性)——攻击者将恶意脚本存储到目标服务器的数据库(如论坛评论、用户昵称)——任何用户浏览到该页面时恶意脚本自动执行。不需要用户点击特定链接——所以危害极大。2005年MySpace的Samy蠕虫是存储型XSS攻击的经典案例——攻击者在个人简介中注入JavaScript——其他用户查看其个人简介后被感染——脚本自动将该用户加为攻击者好友——并在该用户的个人简介中复制同样的恶意代码——形成自我传播——在20小时内感染了超过100万用户。
DOM型XSS——攻击脚本完全在客户端运行——不经过服务器。代码如document.getElementById("output").innerHTML = location.hash——攻击者在URL的#后注入<img src=x onerror=alert(1)>——客户端JavaScript将数据直接赋值给innerHTML——脚本执行。因为攻击载荷不出现在HTTP请求体中——WAF和后端日志都无法察觉——这是DOM型XSS最难防御的原因。
XSS攻击的实际影响
XSS攻击一旦成功——攻击者可以在受害用户的浏览器上下文中执行任意操作:
- 窃取会话凭证——通过
document.cookie读取Cookie(HttpOnly可以阻止但仍有其他方式)或通过XMLHttpRequest向攻击者服务器发送数据 - 页面内容篡改——修改DOM显示虚假信息、注入钓鱼表单骗取密码
- 键盘记录——添加键盘事件监听器——捕捉用户输入的信息(包括密码、信用卡号等敏感数据)
- 内网端口扫描——利用JavaScript向内网资源发送请求——通过超时/错误判定内网存活主机
- DDoS攻击——利用大量被XSS感染的浏览器同时向目标服务器发送请求——形成分布式拒绝服务攻击
防御的纵深层次
| 防御层 | 防御机制 | 绕过难度 |
|---|---|---|
| 输出编码(HTML编码) | 将<>"'&转义为实体 | 低——上下文编码不当时可用的事件属性绕过 |
| CSP(内容安全策略) | 限制脚本执行来源 | 中——JSONP回调或CDN绕过 |
| HttpOnly Cookie | JS无法读取Cookie | 中——其他攻击无需窃取Cookie |
| 输入验证/净化 | 白名单允许的标签和属性 | 高——富文本需要谨慎配置允许规则 |
这些防御层需要叠加使用——单一层防御都可能被绕过——多层防御叠加后攻击者需要同时突破多层限制——大幅降低了成功的概率。
不同上下文中的XSS防御差异
XSS注入点可能位于HTML页面的不同位置——每种位置需要不同的编码规则:
HTML标签体内(<div>USER_INPUT</div>)——必须将< > &转义为< > &——防止用户输入包含的HTML标签被当成真实的HTML解析。
HTML属性内(<input value="USER_INPUT">)——除了转义< > &——还必须转义"为"——否则攻击者可以用"闭合属性——插入事件处理器如onclick=alert(1)。
JavaScript字符串内(<script>var name = 'USER_INPUT';</script>)——需转义' " \ / \n \r \t等——防止用户输入逃出字符串边界执行任意代码。
CSS内(<style>body{background: USER_INPUT}</style>)——CSS上下文可能被利用加载外部恶意样式或发起数据请求——最安全的做法是完全不允许用户控制CSS内容。
XSS的自动化检测方法
构建XSS防御清单是审计的第一步——手动逐点验证不可行——自动化工具用于扫描:
- 静态分析(SAST)——扫描源代码中是否存在用户输入直接拼接内到一个"sink"函数(如
innerHTML、document.write——标记可疑行 - 动态分析(DAST)——向页面注入XSS测试payload——观察页面是否执行了注入的JavaScript——验证反射型/存储型XSS是否存在(DAST通常用于部署测试环境中)
- 模糊测试(Fuzzing)——自动生成大量变异后的payload试探XSS过滤——探测可绕过的编码方式
大型互联网公司往往结合SAST的CI检查+DAST的定期扫描+人工代码审查+SRC漏洞众测等多条路线交叉防护。
标签内XSS的绕过案例
开发者可能做了<和>的转义但忘记对单引号/双引号进行过滤——导致无法逃逸标签之间的开闭、但在属性中的注入仍然可能(如onclick、onload、onerror等事件处理器在部分版本中仍可被注入利用)。自动转义框架(如React默认进行JSX的转义、Angular的DomSanitizer)在多数Web框架中提供了基础编码——但开发者使用了框架的"安全跳过"API(如React的dangerouslySetInnerHTML、Angular的bypassSecurityTrustHtml)时会绕开框架保护——引入XSS漏洞。
XSS与CSP的对抗案例
当CSP设置为script-src 'self'——禁止所有内联脚本、只允许加载同源的JavaScript文件。攻击者仍然可以尝试绕过:
利用同源CDN的库函数——如果站内包含一个允许传入回调参数的JavaScript文件(JSONP接口)——攻击者可以将该文件作为
<script src="/api/jsonp?callback=alert(1)">加载——通过JSONP的回调参数执行JavaScript。利用预先存在的同源JS库——如站内使用特定版本的jQuery——攻击者可以利用
<script src="/jquery.js">加载jQuery——然后通过其他方式(如DOM操作)调用jQuery的函数来执行恶意代码——但这通常需要与其它漏洞(如存储型XSS配合找到jQuery的执行入口)组合利用。利用文件上传功能——如果站点允许上传任意文件并保存在同源目录下——攻击者可以上传一个包含JavaScript的
.js文件到服务器——然后通过<script src="/uploads/malicious.js">加载执行。
CSP的安全性与配置的严格程度直接相关——strict-dynamic、nonce、hash等机制进一步增强了CSP的防御能力——但配置不当的CSP可能几乎不增加安全性(如script-src 'unsafe-inline'会完全丧失CSP对XSS的防御效果)。
使用DOM Purify进行富文本清理
对于需要允许用户使用部分安全的HTML标签(粗体、斜体、列表、表格)的应用场景——不能简单地对所有HTML标签都进行编码(否则富文本功能失效)DOMPurify就是一个专门用于净化HTML的库——它通过白名单机制只保留安全的标签和属性——全面移除事件处理器、javascript:伪链接和经过混淆的攻击向量。
XSS的总结——攻击链与防御要点
攻击链:
用户输入未验证/未编码
用户输入 ──────────────────────→ HTML页面中嵌入
│
呈现在浏览器端
│
恶意脚本在用户上下文中执行
│
窃取Cookie/篡改页面/发起请求
防御链:
用户输入 → 输入验证/净化(白名单) → 输出编码(按上下文选择编码方式)
→ CSP(限制脚本来源和执行) → HttpOnly Cookie(保护会话Token)
→ WAF(检测已知攻击模式)防御链中每一个环节都可以独立防止一部分攻击——组合起来形成纵深防御——任何单一环节被绕过——仍有后续环节可以挡住攻击。XSS防御不是"做一次"的事情——需要在每次数据输出到HTML页面的每个位置都保持警觉。
XSS漏洞与渗透测试的实践要点
在渗透测试/CTF挑战中分析XSS漏洞时——关注以下环节:
1. 确认输入点——用户输入出现在页面的哪个位置?参数、路径、Referer、Cookie、WebSocket消息?
2. 确认输出编码——服务器端是否对用户输入做了编码——如果是——编码的规则是什么(HTML标签体/属性/JS/CSS)?
3. 识别绕过可能——如果做了编码——编码是否可以被绕过(如双编码、Unicode绕过、换行混淆等)
4. 确认CSP策略——HTTP响应头Content-Security-Policy的内容——是否允许内联脚本——外部脚本加载的来源?
5. 确认HttpOnly——Cookie是否设置了HttpOnly标志——决定窃取Session的难度
6. 确认同站点文件上传功能——是否可以上传文件到同源目录下——然后加载文件中的脚本这些检查点贯穿了XSS从发现到利用的完整链条——一个环节突破不了——就换下一个环节尝试或组合其他漏洞使用。
XSS防御清单总结
□ 所有用户输出实施上下文感知的编码(HTML/JS/CSS/URL)
□ 设置Content-Security-Policy响应头——严格限制脚本源——禁止unsafe-inline
□ Cookie始终设置HttpOnly和Secure标志
□ 富文本输入使用白名单HTML净化库(DOMPurify)
□ 避免使用eval、innerHTML、document.write等危险sink
□ 对JSONP、postMessage等接口实施严格验证
□ 定期使用自动化扫描工具(SAST/DAST)检查XSS
□ 代码审查时重点关注用户输入到HTML输出的所有路径这条清单不是为了在生产环境中直接"打勾"——而是为了保证开发阶段就逐项落实——并配合上线的自动化扫描——最后形成阶段性的XSS防御评估报告——在新的功能开发周期开始后这套清单仍会被重新检视。
浏览器XSS过滤机制(XSS Auditor/XSS Filter)
早期浏览器(Chrome的XSS Auditor、IE的XSS Filter)曾尝试在客户端检测并拦截反射型XSS——检查URL中的脚本代码与HTML响应的一致性——如果匹配则阻止页面加载。但XSS Auditor存在被绕过和滥用的问题——也可能阻止合法页面的功能——Chrome在2019年移除了XSS Auditor。这意味着XSS的防御绝不能依赖客户端的安全机制——服务端输出编码和CSP是唯一可靠的防御手段。
复习检查(续五)
为什么不能依赖客户端(XSS Auditor)防御XSS——服务端的输出编码才是根本——因为客户端不知道哪些数据是用户输入的——哪些是服务器正常产生的——且审计到反射型可能因为浏览器的高效执行跳过报警。
在React/Vue/Angular等框架中为什么还会出现XSS——框架默认转义数据绑定——但开发者使用
v-html、dangerouslySetInnerHTML、bypassSecurityTrustHtml等方法时绕开了框架的保护——直接操作DOM时也可能产生XSS——框架不是XSS的绝对保障。一个完整的XSS渗透测试检查步骤——确认输入点→确认输出编码→识别绕过可能→确认CSP→确认HttpOnly→确认上传功能→构造最终利用payload。
XSS的自动化扫描和人工渗透在XSS漏洞发现上的关系——自动化扫描可以覆盖大量已知模式——但只有人工渗透才能发现特定业务逻辑上下文中的绕过方式和新类型的DOM XSS。
XSS和CSRF组合利用——CSRF攻击需要攻击者知道用户的Cookie中Token——如果在登录后再以一个存储XSS获取目标用户在当前网站的Token——后续配合CSRF便可以发起任意操作——这两者的结合在真实攻击战役中经常发生。
复习检查(续四)
复习检查(续三)
开发者可能做了<和>的转义但忘记对单引号/双引号进行过滤——导致无法逃逸标签之间的开闭、但在属性中的注入仍然可能(如onclick、onload、onerror等事件处理器在部分版本中仍可被注入利用)。自动转义框架(如React默认进行JSX的转义、Angular的DomSanitizer)在多数Web框架中提供了基础编码——但开发者使用了框架的"安全跳过"API(如React的dangerouslySetInnerHTML、Angular的bypassSecurityTrustHtml)时会绕开框架保护——引入XSS漏洞。