JavaScript 中预防 XSS(跨站脚本攻击)

108 阅读5分钟

在 JavaScript 中预防 XSS(跨站脚本攻击)是 Web 开发中最关键的安全措施之一。XSS 攻击的本质是攻击者将恶意代码(通常是 JavaScript)注入到网页中,并被其他用户的浏览器执行。

以下是预防 XSS 的主要措施,从最有效到辅助措施排列:

一、 核心原则:对不可信数据进行处理

所有预防措施都围绕一个核心原则:绝不信任用户输入。任何来自用户、第三方或数据库的数据在动态插入到页面之前,都必须被视为不可信的,并经过适当的处理。


二、 具体措施与最佳实践

1. 转义(Escaping) - 最根本的防线

转义是将数据中的危险字符转换为安全的字符实体(HTML Entities),从而避免浏览器将其解析为 HTML 或 JS 代码。

  • 针对 HTML 上下文:当你要将数据插入到 HTML 标签内部或属性中时。

    • 方法:将以下字符进行转义:

      • & -> &
      • < -> &lt;
      • > -> &gt;
      • " -> &quot;
      • ' -> &#x27; (或 &apos;,但后者在 HTML 4 中不推荐)
      • / -> &#x2F; (有助于预防闭合标签)
    • 实践

      • 使用成熟的库,如 DOMPurify 的内部方法,或者简单的工具函数。
      • 现代前端框架(如 React, Vue, Angular)默认已经自动进行了转义,这是它们最大的安全优势之一。除非你使用 dangerouslySetInnerHTML (React) 或 v-html (Vue) 等故意绕过转义的 API。
  • 针对 HTML 属性上下文:同上,确保属性值用引号括起来,并转义引号。

  • 针对 JavaScript 上下文:当你要将数据插入到 <script> 标签或事件处理程序(如 onclick)中时。

    • 方法:极其危险,应尽量避免。如果必须,确保数据被正确编码为 JSON 字符串。
    • 实践:使用 JSON.stringify() 将数据转换为字符串,而不是用字符串拼接的方式构造 JS 代码。
  • 针对 URL 上下文:当你要将数据作为 URL 参数(如 href 或 src 属性)时。

    • 方法:使用 encodeURIComponent() 对参数值进行编码。切勿使用 encodeURI(),它编码的范围更小

    • 例子

      // 错误做法
      let userInput = 'javascript:alert("xss")';
      aTag.href = userInput; // 危险!
      
      // 正确做法:验证协议并编码
      if (userInput.startsWith('https://') || userInput.startsWith('http://')) {
        aTag.href = userInput; // 允许,因为是安全协议
      } else {
        aTag.href = '#'; // 或者禁用
      }
      

2. 内容安全策略(CSP - Content Security Policy)

CSP 是一个强大的、深度防御的 HTTP 响应头。它不直接修复漏洞,而是通过白名单机制告诉浏览器允许加载和执行哪些资源。

  • 工作原理:即使攻击者成功注入了脚本,如果该脚本不在 CSP 白名单内,浏览器也会拒绝执行。

  • 常见指令

    Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.cdn.com; object-src 'none'; style-src 'self' 'unsafe-inline';
    
    • default-src 'self':默认只允许加载同源资源。
    • script-src 'self':只允许执行同源的脚本。可以添加可信的 CDN。
    • script-src 'unsafe-inline':允许内联脚本(如 <script>...</script> 或 onclick 属性)。强烈不建议开启,这会大大削弱 CSP 的效果。现代开发应避免使用内联事件处理程序。
    • object-src 'none':完全禁止 <object><embed><applet> 等,减少攻击面。
    • style-src 'self' 'unsafe-inline':允许同源和内联样式(很常见,风险相对较低)。

3. 输入验证与过滤

在数据入库或进行处理之前,对其进行严格的验证。

  • 类型验证:确保数据符合预期的格式(如邮箱、电话号码、数字等)。
  • 长度限制:防止过长的输入。
  • 白名单原则:只允许已知安全的字符通过,比黑名单(试图过滤危险字符)要有效得多。例如,用户名字段可以只允许字母、数字和特定符号。
  • 注意:输入验证不能替代输出转义。因为数据的用途可能会改变(比如原本在文本上下文的数据后来被用到了 HTML 上下文)。

4. 使用安全的 DOM API

避免使用那些容易引发 XSS 的旧 API,转而使用更安全的现代 API。

  • 避免使用

    • element.innerHTML = userData; // 极其危险!
    • document.write(userData); // 极其危险!
    • eval(userData); // 绝对禁止!
    • setTimeout(userData, 100); // 危险!
  • 推荐使用

    • element.textContent = userData; // 安全!它会将内容作为纯文本插入,不会被解析为 HTML。
    • element.setAttribute(‘alt’, userData); // 相对安全,但最好还是对 userData 进行转义。
    • 操作 innerHTML 时,务必先对 userData 进行净化(见下一条)。

5. 净化(Sanitization) - 当需要富文本时

有时你的应用需要用户输入 HTML(如博客评论、富文本编辑器)。这时转义会破坏所需的格式,你需要的是“净化”。

  • 方法:使用一个强大的、专门的白名单净化库来过滤用户输入的 HTML。

    • 首选库DOMPurify。它是一个轻量、快速且经过严格测试的库。它会移除所有危险的标签和属性,只保留安全的子集(如 <b><i><a href="...">)。
  • 实践

    // 使用 DOMPurify
    const cleanHTML = DOMPurify.sanitize(dirtyHTML);
    div.innerHTML = cleanHTML; // 现在安全了
    

    切勿尝试自己用正则表达式写一个 HTML 净化器!  HTML 的语法非常复杂,很容易被绕过。

6. 设置安全的 Cookie 属性

防止攻击者通过 XSS 窃取用户的 Cookie(特别是 Session ID)。

  • HttpOnly:这是最重要的属性。设置了 HttpOnly 的 Cookie 无法通过 JavaScript 的 document.cookie API 访问,从而有效防止被窃取。
  • Secure:确保 Cookie 只通过 HTTPS 协议传输。
  • SameSite:可以设置为 Strict 或 Lax,防止 CSRF 攻击,并在一定程度上增加 XSS 的利用难度。

示例 Set-Cookie 响应头:

Set-Cookie: sessionId=abc123; Secure; HttpOnly; SameSite=Strict

总结

措施描述适用场景
转义 (Escaping)将危险字符转换为实体所有动态内容输出到 HTML 时
CSPHTTP 头,定义资源白名单所有生产环境应用,深度防御
输入验证检查数据格式和长度数据录入阶段,减少无效数据
安全 DOM API使用 textContent 而非 innerHTML插入纯文本内容时
净化 (Sanitization)使用 DOMPurify 过滤 HTML必需处理富文本内容时
HttpOnly Cookie防止 JS 读取敏感 Cookie所有身份验证相关的 Cookie

最有效的策略是  “默认转义” + “CSP”  的组合。现代前端框架帮你处理了大部分转义工作,而 CSP 则作为最后一道坚固的防线。对于富文本等特殊情况,则毫不犹豫地使用 DOMPurify