各位靓仔靓女,早上好/下午好/晚上好!
今天咱们聊点安全又有趣的东西:JavaScript 的 CSP,也就是内容安全策略 (Content Security Policy)。这玩意儿听起来高大上,其实就是给你的网站穿上一层防护衣,防止坏人搞破坏。
一、 什么是 CSP? 为什么要用它?
想象一下,你的网站是个大Party,谁都可以来。但是,有些不速之客可能会偷偷往你的鸡尾酒里下毒 (比如插入恶意脚本)。CSP就像是你的Party保安,严格规定哪些人 (哪些来源) 可以提供饮料、音乐、甚至跳舞 (执行脚本)。
具体来说,CSP是一种基于 HTTP 响应头的安全策略,它告诉浏览器,只允许加载来自特定来源的资源。这些资源包括 JavaScript、CSS、图片、字体等等。 浏览器会检查每个资源的来源,如果来源不在 CSP 策略允许的范围内,浏览器就会阻止该资源的加载和执行。
为什么要用 CSP?
- 防止跨站脚本攻击 (XSS): 这是最主要的目的。XSS 攻击是指攻击者将恶意脚本注入到你的网站中,让用户在不知情的情况下执行这些脚本。CSP 可以通过限制脚本的来源,有效地防御 XSS 攻击。
- 减少数据包嗅探风险: 攻击者可能通过嗅探网络流量来获取用户数据。CSP 可以强制浏览器使用 HTTPS 连接,加密数据传输,从而减少数据包嗅探的风险。
- 防止点击劫持: 点击劫持是指攻击者将你的网站隐藏在另一个网站的透明层之下,诱使用户点击他们不想点击的链接。CSP 可以通过
frame-ancestors
指令,限制哪些网站可以嵌入你的网站,从而防止点击劫持。 - 提升网站性能: CSP 可以强制浏览器只加载来自可信来源的资源,减少加载恶意资源的可能性,从而提升网站性能。
- 符合安全标准: 许多安全标准 (比如 PCI DSS) 都要求网站实施内容安全策略。
二、 CSP 的配置方式:HTTP 响应头 vs. <meta>
标签
配置 CSP 有两种主要方式:
-
HTTP 响应头: 这是推荐的方式。通过服务器配置,在 HTTP 响应头中添加
Content-Security-Policy
或Content-Security-Policy-Report-Only
字段。 -
<meta>
标签: 可以在 HTML 的<head>
部分使用<meta>
标签来定义 CSP。但这种方式的限制较多,比如不能应用于frame-ancestors
指令。
HTTP 响应头配置示例 (Node.js):
const express = require('express');
const app = express();
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy',
"default-src 'self'; script-src 'self' 'unsafe-inline' https://example.com; style-src 'self' https://cdn.example.com; img-src 'self' data:;"
);
next();
});
app.get('/', (req, res) => {
res.send('Hello World!');
});
app.listen(3000, () => {
console.log('Server listening on port 3000');
});
<meta>
标签配置示例:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>CSP Example</title>
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self'">
</head>
<body>
<h1>Hello, CSP!</h1>
<script>
console.log("Inline script is allowed.");
</script>
<style>
body { font-family: sans-serif; }
</style>
</body>
</html>
三、 CSP 指令详解:打造你的专属防护盾
CSP 的核心在于各种指令,它们定义了允许加载的资源类型以及来源。 常见的指令包括:
指令 | 描述 | 示例 |
---|---|---|
default-src |
定义所有其他指令未明确定义的资源类型的默认来源。 这是一个兜底策略。 | default-src 'self' (只允许来自同一来源的资源) |
script-src |
定义 JavaScript 脚本的有效来源。 | script-src 'self' https://cdn.example.com (允许来自同一来源和 https://cdn.example.com 的脚本) |
style-src |
定义 CSS 样式的有效来源。 | style-src 'self' 'unsafe-inline' (允许来自同一来源的样式,以及内联样式) |
img-src |
定义图片的有效来源。 | img-src 'self' data: (允许来自同一来源的图片,以及 data: URI 格式的图片) |
font-src |
定义字体的有效来源。 | font-src 'self' https://fonts.example.com (允许来自同一来源和 https://fonts.example.com 的字体) |
connect-src |
定义可以建立连接 (例如,通过 XMLHttpRequest 、WebSocket 、EventSource ) 的有效来源。 |
connect-src 'self' https://api.example.com (允许连接到同一来源和 https://api.example.com ) |
frame-src |
定义可以嵌入当前页面的 <frame> 、<iframe> 、<object> 、<embed> 或 <applet> 元素的有效来源。 已经被 child-src 替代,不推荐使用 |
frame-src 'self' https://example.com (允许嵌入来自同一来源和 https://example.com 的页面) |
child-src |
定义可以嵌入当前页面的 Web Workers 和嵌套的浏览上下文 (例如,<iframe> ) 的有效来源。 |
child-src 'self' https://example.com (允许嵌入来自同一来源和 https://example.com 的页面) |
frame-ancestors |
定义允许嵌入当前页面的来源。 这用于防止点击劫持攻击。 这个指令只能通过HTTP头来配置 | frame-ancestors 'self' https://example.com (只允许同一来源和 https://example.com 的页面嵌入当前页面) |
form-action |
定义表单可以提交到的有效 URI。 | form-action 'self' https://example.com/submit (只允许提交到同一来源和 https://example.com/submit ) |
base-uri |
定义 <base> 元素可以使用的有效 URI。 |
base-uri 'self' (只允许使用同一来源的 URI 作为 <base> 元素) |
object-src |
定义 <object> 、<embed> 和 <applet> 元素的有效来源。 |
object-src 'none' (不允许加载任何插件) |
media-src |
定义 <audio> 、<video> 和 <track> 元素的有效来源。 |
media-src 'self' (只允许加载来自同一来源的媒体文件) |
worker-src |
定义 Worker 脚本的有效来源。 | worker-src 'self' (只允许加载来自同一来源的 Worker 脚本) |
manifest-src |
定义应用缓存 manifest 文件的有效来源。 | manifest-src 'self' (只允许加载来自同一来源的 manifest 文件) |
upgrade-insecure-requests |
指示浏览器将所有不安全的 URL (HTTP) 升级为安全的 URL (HTTPS)。 | upgrade-insecure-requests (强制浏览器使用 HTTPS) |
block-all-mixed-content |
阻止加载任何通过 HTTP 加载的资源,如果页面是通过 HTTPS 加载的。 | block-all-mixed-content (阻止混合内容) |
plugin-types |
定义可以加载的插件类型。 | plugin-types application/pdf application/x-shockwave-flash (只允许加载 PDF 和 Flash 插件) |
sandbox |
为请求的资源启用沙箱。 这类似于 <iframe> 标签的 sandbox 属性。 |
sandbox allow-forms allow-scripts (允许表单提交和脚本执行) |
report-uri |
指定一个 URI,浏览器会将违反 CSP 策略的报告发送到该 URI。 已经被 report-to 替代,不推荐使用 |
report-uri /csp-report (将报告发送到 /csp-report 路径) |
report-to |
指定一个或多个端点组,浏览器会将违反 CSP 策略的报告发送到这些端点组。 允许更灵活地配置报告目标。 | report-to csp-endpoint (将报告发送到名为 csp-endpoint 的端点组) |
指令值:
'self'
: 允许来自同一来源的资源 (协议、域名和端口都必须相同)。'none'
: 不允许加载任何资源。'unsafe-inline'
: 允许内联 JavaScript 和 CSS。 强烈不推荐使用,因为它会削弱 CSP 的防御 XSS 攻击的能力。'unsafe-eval'
: 允许使用eval()
和相关函数。 同样不推荐使用,因为它也会削弱 CSP 的防御 XSS 攻击的能力。'unsafe-hashes'
:允许特定的内联事件处理程序,通常用于过渡性地迁移到更安全的替代方案。需要指定事件处理程序的 SHA256、SHA384 或 SHA512 哈希值。data:
: 允许使用data:
URI 格式的资源 (比如嵌入在 HTML 中的图片)。mediastream:
: 允许使用mediastream:
URI 格式的资源 (用于访问用户摄像头和麦克风)。blob:
: 允许使用blob:
URI 格式的资源 (用于创建客户端文件)。filesystem:
: 允许使用filesystem:
URI 格式的资源 (用于访问文件系统 API)。https://example.com
: 允许来自特定域名 (包括协议) 的资源。*.example.com
: 允许来自特定域名及其所有子域的资源。nonce-<base64-value>
: 允许具有匹配 nonce 属性的脚本或样式。 这是一种更安全的方式来允许内联脚本和样式,但需要在服务器端生成随机 nonce 值,并在 HTML 和 CSP 策略中使用相同的 nonce 值。sha256-<base64-value>
,sha384-<base64-value>
,sha512-<base64-value>
: 允许具有匹配哈希值的脚本或样式。 同样是一种更安全的方式来允许内联脚本和样式,但需要计算脚本或样式的哈希值,并在 CSP 策略中使用该哈希值。'strict-dynamic'
:允许由可信脚本创建的脚本加载其他脚本,而无需显式地列出这些脚本的来源。 需要与 nonce 或 hash 关键字一起使用。'report-sample'
:指示浏览器在违规报告中包含违规资源的样本。
四、 CSP 的部署策略:循序渐进,步步为营
部署 CSP 不是一蹴而就的事情,需要循序渐进,逐步加强策略的严格程度。一个比较好的策略是:
- 评估现有资源: 梳理你的网站使用了哪些资源,它们的来源是什么。
- 使用
Content-Security-Policy-Report-Only
模式: 在这种模式下,浏览器不会阻止违反 CSP 策略的资源,而是将违规报告发送到你指定的 URI。你可以通过分析这些报告,了解哪些资源违反了策略,并据此调整策略。
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy-Report-Only',
"default-src 'self'; script-src 'self' 'unsafe-inline' https://example.com; report-uri /csp-report"
);
next();
});
- 逐步收紧策略: 根据违规报告,逐步收紧 CSP 策略,比如移除
'unsafe-inline'
和'unsafe-eval'
,并尽可能使用 nonce 或 hash 来允许内联脚本和样式。 - 切换到
Content-Security-Policy
模式: 当你确信 CSP 策略已经足够完善时,就可以切换到Content-Security-Policy
模式,让浏览器真正开始阻止违反策略的资源。
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy',
"default-src 'self'; script-src 'self' https://example.com 'nonce-r4nd0m'; style-src 'self' 'nonce-r4nd0m'; report-uri /csp-report"
);
next();
});
五、 使用 Nonce 和 Hash:更安全的内联脚本和样式
正如前面提到的,'unsafe-inline'
会削弱 CSP 的防御 XSS 攻击的能力。 为了更安全地允许内联脚本和样式,可以使用 nonce 或 hash。
使用 Nonce:
- 在服务器端生成随机 nonce 值:
const crypto = require('crypto');
function generateNonce() {
return crypto.randomBytes(16).toString('base64');
}
app.use((req, res, next) => {
const nonce = generateNonce();
res.locals.nonce = nonce;
res.setHeader(
'Content-Security-Policy',
`default-src 'self'; script-src 'self' https://example.com 'nonce-${nonce}'; style-src 'self' 'nonce-${nonce}'; report-uri /csp-report`
);
next();
});
- 在 HTML 中使用相同的 nonce 值:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>CSP Example with Nonce</title>
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'nonce-{{nonce}}'">
</head>
<body>
<h1>Hello, CSP with Nonce!</h1>
<script nonce="{{nonce}}">
console.log("Inline script is allowed with nonce.");
</script>
<style nonce="{{nonce}}">
body { font-family: sans-serif; }
</style>
</body>
</html>
(注意:上面的 {{nonce}}
是一种模板语法,你需要根据你使用的模板引擎进行替换。)
使用 Hash:
- 计算脚本或样式的 SHA256、SHA384 或 SHA512 哈希值: 可以使用在线工具或命令行工具来计算哈希值。 例如,使用 OpenSSL:
openssl dgst -sha256 -binary < inline-script.js | openssl base64
- 在 CSP 策略中使用哈希值:
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy',
"default-src 'self'; script-src 'self' 'sha256-YOUR_SCRIPT_HASH'; style-src 'self' 'sha256-YOUR_STYLE_HASH'; report-uri /csp-report"
);
next();
});
- 在 HTML 中使用内联脚本和样式:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>CSP Example with Hash</title>
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'sha256-YOUR_SCRIPT_HASH'">
</head>
<body>
<h1>Hello, CSP with Hash!</h1>
<script>
console.log("Inline script is allowed with hash.");
</script>
<style>
body { font-family: sans-serif; }
</style>
</body>
</html>
(将 YOUR_SCRIPT_HASH
和 YOUR_STYLE_HASH
替换为你计算出的实际哈希值。)
六、 处理 CSP 违规报告:追踪安全漏洞
当浏览器检测到违反 CSP 策略的资源时,它会生成一个 JSON 格式的违规报告,并将其发送到你指定的 report-uri
或 report-to
。 你需要设置一个端点来接收和处理这些报告。
Node.js 示例:
app.use(express.json()); // 解析 JSON 格式的请求体
app.post('/csp-report', (req, res) => {
console.log('CSP Violation Report:', req.body);
// 将报告保存到数据库或发送到安全监控系统
res.status(204).end(); // 必须返回 204 No Content 状态码
});
违规报告示例:
{
"csp-report": {
"document-uri": "https://example.com/",
"referrer": "",
"violated-directive": "script-src 'self' https://example.com",
"effective-directive": "script-src",
"original-policy": "default-src 'self'; script-src 'self' https://example.com; report-uri /csp-report",
"blocked-uri": "https://evil.com/malicious.js",
"status-code": 200,
"script-sample": ""
}
}
通过分析违规报告,你可以了解:
- 哪个资源违反了 CSP 策略 (
blocked-uri
) - 哪个指令被违反了 (
violated-directive
) - 违反策略的页面 (
document-uri
) - 导致违规的引用页面 (
referrer
)
七、 CSP 的最佳实践:打造坚不可摧的安全防线
- 使用 HTTP 响应头配置 CSP: 这是推荐的方式,因为它更灵活,可以应用于所有指令。
- 从严格的策略开始,逐步放宽: 这有助于你更好地了解你的网站需要哪些资源,并避免一开始就阻止了必要的资源。 当然,从宽松策略开始,逐步收紧也是一种方法,取决于你的团队的风格和项目的具体情况。
- 尽可能避免使用
'unsafe-inline'
和'unsafe-eval'
: 它们会削弱 CSP 的防御 XSS 攻击的能力。 - 使用 nonce 或 hash 来允许内联脚本和样式: 这是更安全的方式。
- 监控 CSP 违规报告: 及时发现和修复安全漏洞。
- 定期审查和更新 CSP 策略: 随着你的网站不断发展,你需要定期审查和更新 CSP 策略,以确保它仍然有效。
- 使用 CSP 兼容性工具: 有一些在线工具可以帮助你检查 CSP 策略的兼容性,确保它在不同的浏览器中都能正常工作。
- 考虑使用 SRI (Subresource Integrity): SRI 可以验证从 CDN 加载的资源的完整性,防止 CDN 被攻击后,你的网站也受到影响。 通过
<script>
和<link>
标签的integrity
属性,可以指定资源的哈希值,浏览器会验证加载的资源是否与哈希值匹配。
八、 总结:安全之路,永无止境
CSP 是一种强大的安全工具,可以有效地防御 XSS 攻击和其他安全威胁。 但是,它不是万能的。你需要结合其他安全措施,比如输入验证、输出编码、漏洞扫描等等,才能真正保护你的网站安全。
记住,安全之路,永无止境! 不要掉以轻心,要时刻保持警惕,才能让你的网站安全无虞。
今天就到这里,希望大家有所收获! 如果有什么问题,欢迎随时提问。 祝大家编码愉快,永不 Bug!