在 JavaScript 中预防 XSS(跨站脚本攻击)是 Web 开发中最关键的安全措施之一。XSS 攻击的本质是攻击者将恶意代码(通常是 JavaScript)注入到网页中,并被其他用户的浏览器执行。
以下是预防 XSS 的主要措施,从最有效到辅助措施排列:
一、 核心原则:对不可信数据进行处理
所有预防措施都围绕一个核心原则:绝不信任用户输入。任何来自用户、第三方或数据库的数据在动态插入到页面之前,都必须被视为不可信的,并经过适当的处理。
二、 具体措施与最佳实践
1. 转义(Escaping) - 最根本的防线
转义是将数据中的危险字符转换为安全的字符实体(HTML Entities),从而避免浏览器将其解析为 HTML 或 JS 代码。
-
针对 HTML 上下文:当你要将数据插入到 HTML 标签内部或属性中时。
-
方法:将以下字符进行转义:
&
->&
<
-><
>
->>
"
->"
'
->'
(或'
,但后者在 HTML 4 中不推荐)/
->/
(有助于预防闭合标签)
-
实践:
- 使用成熟的库,如
DOMPurify
的内部方法,或者简单的工具函数。 - 现代前端框架(如 React, Vue, Angular)默认已经自动进行了转义 ,这是它们最大的安全优势之一。除非你使用
dangerouslySetInnerHTML
(React) 或v-html
(Vue) 等故意绕过转义的 API。
- 使用成熟的库,如
-
-
针对 HTML 属性上下文:同上,确保属性值用引号括起来,并转义引号。
-
针对 JavaScript 上下文 :当你要将数据插入到
<script>
标签或事件处理程序(如onclick
)中时。- 方法:极其危险,应尽量避免。如果必须,确保数据被正确编码为 JSON 字符串。
- 实践 :使用
JSON.stringify()
将数据转换为字符串,而不是用字符串拼接的方式构造 JS 代码。
-
针对 URL 上下文 :当你要将数据作为 URL 参数(如
href
或src
属性)时。-
方法 :使用
encodeURIComponent()
对参数值进行编码。切勿使用encodeURI()
,它编码的范围更小。 -
例子:
javascript// 错误做法 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 白名单内,浏览器也会拒绝执行。
-
常见指令:
httpContent-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 。它是一个轻量、快速且经过严格测试的库。它会移除所有危险的标签和属性,只保留安全的子集(如
-
实践:
javascript// 使用 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 响应头:
http
Set-Cookie: sessionId=abc123; Secure; HttpOnly; SameSite=Strict
总结
措施 | 描述 | 适用场景 |
---|---|---|
转义 (Escaping) | 将危险字符转换为实体 | 所有动态内容输出到 HTML 时 |
CSP | HTTP 头,定义资源白名单 | 所有生产环境应用,深度防御 |
输入验证 | 检查数据格式和长度 | 数据录入阶段,减少无效数据 |
安全 DOM API | 使用 textContent 而非 innerHTML |
插入纯文本内容时 |
净化 (Sanitization) | 使用 DOMPurify 过滤 HTML | 必需处理富文本内容时 |
HttpOnly Cookie | 防止 JS 读取敏感 Cookie | 所有身份验证相关的 Cookie |
最有效的策略是 "默认转义" + "CSP" 的组合。现代前端框架帮你处理了大部分转义工作,而 CSP 则作为最后一道坚固的防线。对于富文本等特殊情况,则毫不犹豫地使用 DOMPurify。