Content Security Policy (CSP) 是一种重要的安全机制,旨在帮助防止跨站脚本 (XSS) 和其他代码注入攻击。它通过允许开发者定义浏览器可以加载哪些资源以及从何处加载这些资源来工作。CSP 可以作为 HTTP 响应头或 <meta>
标签配置。通常,HTTP 响应头是首选方式,因为它提供了更全面的控制和安全性,而 <meta>
标签有一些限制,例如不支持 frame-ancestors
、report-to
、report-uri
和 sandbox
等指令 [1][2]。
CSP 配置基础
CSP 通过一系列指令来定义策略,每个指令控制不同类型的资源加载。这些指令之间用分号 ;
分隔。例如,script-src
用于控制 JavaScript 的来源,style-src
用于控制 CSS 的来源,img-src
用于控制图片的来源等。default-src
指令则为所有未明确指定来源的资源类型设置默认策略 [3][4]。
示例:
一个基本的 CSP 策略可能看起来像这样,它只允许从当前域名加载脚本、图片、CSS 和 AJAX 请求,并禁止加载其他任何资源:
Content-Security-Policy: default-src 'self';
[4]
其中:
'self'
:表示允许来自当前源(同协议、同域名、同端口)的资源。需要加引号。*
:允许来自任何源的资源(不推荐用于敏感资源如脚本) [7].'none'
:禁止加载任何资源。需要加引号 [5][6].
禁止内联脚本
内联脚本(<script>
标签内的代码、HTML 属性中的事件处理程序如 onclick
、javascript:
URL 以及 eval()
等动态代码执行函数)是 XSS 攻击的常见载体,因为浏览器无法区分合法内联脚本与恶意注入的脚本 [3][8]。
默认情况下,严格的 CSP 会禁止内联脚本的执行。这是 CSP 最重要的安全功能之一 [5][8]。
要禁止内联脚本,你可以在 script-src
指令中不包含 'unsafe-inline'
。如果你的策略中没有显式允许内联脚本,它们就会被禁止。
示例:
以下策略禁止所有内联脚本和 eval()
:
Content-Security-Policy: script-src 'self'
这个策略只允许加载来自当前域名的外部脚本文件,而任何内联脚本都将被阻止 [9].
如果你的代码中存在内联脚本,浏览器会报告 CSP 违规错误,并且这些脚本将不会执行。为了符合 CSP,建议将内联脚本重构为外部文件 [8].
允许特定内联脚本
尽管禁止内联脚本是最佳实践,但在某些情况下,你可能无法完全移除所有内联脚本。CSP 提供了两种安全的方式来允许特定的内联脚本执行:Nonce (随机数) 和 Hash (哈希值) [6][8]。
1. 使用 Nonce (随机数)
Nonce 是一种"只使用一次的数字",它是一个加密安全的随机字符串,每次页面加载时都会重新生成。通过将 Nonce 添加到 CSP 策略和对应的 <script>
标签中,你可以允许特定的内联脚本执行 [8][10]。
配置步骤:
-
服务器端生成 Nonce: 在服务器端为每个请求生成一个唯一的、不可猜测的 Base64 编码随机字符串。这个 Nonce 每次页面加载时都必须不同 [8][10]。
- 例如 (Node.js):
const crypto = require("crypto"); const nonce = crypto.randomBytes(16).toString("base64");
[10]
- 例如 (Node.js):
-
将 Nonce 添加到 CSP 头部: 将生成的 Nonce 值添加到
Content-Security-Policy
响应头的script-src
指令中,前缀为'nonce-'
[8][10]。Content-Security-Policy: script-src 'nonce-YOUR_GENERATED_NONCE' 'self';
-
将 Nonce 添加到
<script>
标签: 在需要允许的内联<script>
标签上添加nonce
属性,其值与 CSP 头部中的 Nonce 相同 [8][10]。<script nonce="YOUR_GENERATED_NONCE"> /* 你的内联代码 */ </script>
示例:
假设服务器生成了一个 Nonce 值为 EDNnf03nceIOfn39fn3e9h3sdfa
。
HTTP 响应头:
Content-Security-Policy: script-src 'nonce-EDNnf03nceIOfn39fn3e9h3sdfa' 'self';
[8]
HTML 中的内联脚本:
<script nonce="EDNnf03nceIOfn39fn3e9h3sdfa"> console.log('This inline script is allowed.'); </script>
[8]
优点: 动态性强,每次请求 Nonce 都不同,攻击者难以预测 [11]。
缺点: 需要服务器端动态生成和插入 Nonce,对于静态 HTML 文件或 CDN 部署可能不方便 [11]。
2. 使用 Hash (哈希值)
哈希值是内联脚本内容的 SHA-256、SHA-384 或 SHA-512 散列值。通过计算内联脚本的哈希值并将其添加到 CSP 策略中,可以允许该特定脚本执行 [6][8]。
配置步骤:
-
计算内联脚本的哈希值:
-
将哈希值添加到 CSP 头部: 将计算出的哈希值添加到
script-src
指令中,前缀为'sha256-'
、'sha384-'
或'sha512-'
[6][8].Content-Security-Policy: script-src 'sha256-YOUR_SCRIPT_HASH' 'self';
示例:
假设你的 HTML 中有以下内联脚本:
<script>alert('Hello, world.');</script>
这个脚本的 SHA-256 哈希值可能是 qznLcsROx4GACP2dm0UCKCzCG-HiZ1guq6ZZDob_Tng=
。
HTTP 响应头:
Content-Security-Policy: script-src 'sha256-qznLcsROx4GACP2dm0UCKCzCG-HiZ1guq6ZZDob_Tng=' 'self';
[8]
优点: 适用于静态 HTML 页面,无需服务器端动态生成 [11]。
缺点: 任何对内联脚本内容的微小改动(包括空格和注释)都会改变哈希值,导致脚本被阻止,需要重新计算和更新 CSP [8].
严格 CSP (Strict CSP)
为了更全面地防御 XSS,推荐使用"严格 CSP"。严格 CSP 通常结合 Nonce 或 Hash 以及 strict-dynamic
指令。strict-dynamic
指令允许通过 Nonce 或 Hash 信任的脚本,动态地加载和执行其他脚本,而无需为每个动态加载的脚本显式添加 Nonce 或 Hash [11][13]。
示例 (基于 Nonce 的严格 CSP):
Content-Security-Policy: script-src 'nonce-{RANDOM}' 'strict-dynamic'; object-src 'none'; base-uri 'none';
[11]
其中 {RANDOM}
每次请求都会替换为新的 Nonce。
CSP 报告机制
为了监控 CSP 违规情况,你可以使用 report-uri
或 report-to
指令。当浏览器检测到 CSP 策略被违反时,它会将违规报告(JSON 格式)发送到指定的 URI [14][15]。
示例:
Content-Security-Policy: default-src 'self'; script-src 'self'; report-uri /csp-report-endpoint;
[16]
注意: report-uri
已被 report-to
取代,但 report-uri
仍然广泛使用 [6][14]. 报告端点应使用 HTTPS 以保护报告数据的隐私 [17].
实施建议
- 从 Report-Only 模式开始: 在生产环境中强制执行 CSP 之前,建议先使用
Content-Security-Policy-Report-Only
头部。这允许你监控 CSP 违规,而不会阻止任何内容加载,从而帮助你发现并调整策略,避免影响用户体验 [1][4].
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; report-uri /csp-report-endpoint;
- 逐步收紧策略: 从一个宽松的策略开始,然后逐步收紧,直到所有合法的资源都得到允许,并且所有不必要的资源都被阻止 [1].
- 避免
'unsafe-inline'
和'unsafe-eval'
: 除非绝对必要,否则应避免在script-src
或style-src
中使用'unsafe-inline'
和'unsafe-eval'
。它们会大大削弱 CSP 的保护作用 [8]. - 定期审查和更新: 随着网站功能的增加和依赖项的变化,CSP 策略也需要定期审查和更新 [1].
通过正确配置和使用 CSP,可以显著提高前端应用的安全性,有效抵御 DOM 型 XSS 等攻击。
推荐好文: