Back

JavaScript 开发者的安全编码实践

JavaScript 开发者的安全编码实践

JavaScript 在浏览器运行的任何地方都能执行,这使其成为软件开发中最易受攻击的领域之一。大多数漏洞并非来自复杂的攻击手段,而是源于日常代码中可预测的模式。本指南涵盖了当你的代码直接在浏览器中运行时最重要的 JavaScript 安全编码实践。

核心要点

  • 基于 DOM 的 XSS 是最常见的 JavaScript 漏洞之一——避免将不可信数据传递给像 innerHTMLeval()document.write() 这样的危险接收点
  • 对于用户提供的数据使用 textContent 而非 innerHTML,当确实需要渲染 HTML 时使用 DOMPurify 进行净化处理
  • 永远不要对动态输入使用 eval()Function() 或基于字符串的 setTimeout/setInterval
  • 使用 nonce 或 hash 强制执行内容安全策略(CSP),并结合 Trusted Types 实现纵深防御
  • 避免在 localStoragesessionStorage 中存储身份验证令牌或密钥——对于会话标识符优先使用 HttpOnly cookie
  • 处理 postMessage 事件时始终验证 event.origin
  • 定期审计依赖树,并对从 CDN 加载的脚本使用子资源完整性验证

基于 DOM 的 XSS:最常见的 JavaScript 漏洞之一

在 JavaScript 中防止 XSS 首先要理解它实际上是如何产生的。基于 DOM 的 XSS 发生在你的代码从攻击者可控的来源读取数据时——比如 location.hashdocument.referrerURLSearchParams——并以不安全的方式将其写入页面。

核心原则:永远不要将不可信数据传递给会解释 HTML 或执行代码的接收点。

应避免与动态数据一起使用的不安全接收点:

// ❌ 以下所有方法都可能执行注入的脚本
element.innerHTML = userInput
element.outerHTML = userInput
document.write(userInput)
eval(userInput)
setTimeout(userInput, 0)       // 仅限字符串形式
new Function(userInput)()

安全的替代方案:

// ✅ 将内容视为文本,永远不作为标记处理
element.textContent = userInput
element.innerText = userInput

当你确实需要渲染 HTML 时——例如富文本编辑器——首先使用 DOMPurify 进行净化:

// ✅ 安全的富文本渲染
element.innerHTML = DOMPurify.sanitize(userInput)

同样的逻辑也适用于 setAttribute。像 hrefsrc 和事件处理器(onclickonload)这样的动态属性可以执行 JavaScript。在处理用户提供的值时,坚持使用静态的、不可执行的属性。

动态代码执行始终不安全

eval()Function() 和基于字符串的 setTimeout/setInterval 不仅是不良实践——它们是直接的代码注入途径。没有安全的方法可以将它们与不可信输入一起使用。

如果你要解析数据,使用 JSON.parse()。如果你需要动态行为,使用数据结构和显式逻辑,而不是运行时代码生成。

内容安全策略作为防御层

内容安全策略(CSP) 限制了哪些脚本可以运行以及它们可以从哪里加载。使用 nonce 或 hash 的严格 CSP——而不是 'unsafe-inline'——可以显著减少任何漏网 XSS 的影响范围。

在支持的浏览器中将 CSP 与 Trusted Types 配对使用,以在 API 级别强制执行安全的 DOM 写入。Trusted Types 的浏览器支持持续扩展,可以在 webstatus.dev 上跟踪。

安全的浏览器 API:Cookie 和客户端存储

持有会话标识符的 Cookie 应始终携带 HttpOnly(阻止 JavaScript 访问)、Secure(仅 HTTPS)和 SameSite=StrictLax(帮助缓解 CSRF)。在服务器端设置这些属性比从 JavaScript 设置更可靠。

localStoragesessionStorage 可被页面上的任何脚本访问。避免在其中存储身份验证令牌、会话密钥或敏感用户数据——XSS 漏洞会立即暴露存储中的所有内容。

使用 postMessage 进行跨域消息传递

postMessage 很有用但容易被误用。在处理传入消息的数据之前,始终验证其 origin:

window.addEventListener('message', (event) => {
  // ✅ 处理前始终验证来源
  if (event.origin !== 'https://trusted-origin.com') return

  handleMessage(event.data)
})

发送消息时,除非你确实没有固定目标,否则避免使用 '*' 作为 targetOrigin。在接收端,始终验证 event.origin 以确保消息来自可信站点。关于安全使用的更多详情请参见 postMessage 文档

JavaScript 供应链安全

JavaScript 供应链安全是一个日益严重的问题。单个被入侵或恶意的包可能影响数千个应用程序。实用步骤:

  • 运行 npm audit 或使用 Snyk 来捕获依赖项中的已知漏洞
  • 提交你的锁文件(package-lock.jsonyarn.lock)并将意外更改视为危险信号
  • 对从 CDN 加载的任何脚本使用子资源完整性(SRI) 哈希
  • 在添加新包之前进行审计——检查下载次数、维护活动以及包名是否可能是拼写错误攻击

快速参考:安全与不安全模式对比

不安全安全替代方案
innerHTML = userInputtextContent = userInput
eval(str)JSON.parse(str)
setTimeout(str, n)setTimeout(fn, n)
令牌存储在 localStorageHttpOnly cookie
不检查来源的 message先验证 event.origin

结论

大多数 JavaScript 漏洞遵循相同的模式:不可信数据在没有验证的情况下到达强大的 API。养成这样的习惯:问”这个值来自哪里,这个 API 可以用它做什么?”持续应用这个问题,可以在发布前捕获大多数问题。

常见问题

并非总是如此,但当你向它传递源自用户输入或任何外部来源的数据时就是不安全的。如果必须渲染动态 HTML,请先使用 DOMPurify 等库对其进行净化。对于纯文本内容,使用 textContent,它永远不会解释标记或执行脚本。

localStorage 可被页面上运行的任何 JavaScript 访问。如果攻击者利用 XSS 漏洞,他们可以读取存储中的所有内容,包括你的令牌。HttpOnly cookie 是更安全的选择,因为 JavaScript 根本无法访问它们,这限制了客户端攻击造成的损害。

反射型 XSS 涉及发送到服务器的恶意负载并在响应中回显。基于 DOM 的 XSS 从不到达服务器。相反,客户端 JavaScript 从攻击者可控的来源(如 URL 片段)读取数据,并以不安全的方式将其写入页面。两者都很危险,但基于 DOM 的 XSS 更难在服务器端检测。

CSP 告诉浏览器允许执行哪些脚本源。使用 nonce 或 hash 的严格策略会阻止内联脚本和未经授权的外部脚本运行。即使攻击者将恶意标记注入页面,浏览器也会拒绝执行它,因为它不符合策略。

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Check our GitHub repo and join the thousands of developers in our community.

OpenReplay