作为 Web 开发工程师,我们时刻面临着各种安全威胁,其中跨站脚本攻击(XSS)是最常见也是危害最大的漏洞之一。攻击者通过注入恶意脚本,可以窃取用户敏感信息、劫持会话甚至篡改网页内容。传统的防御手段(如输入验证和输出编码)虽然重要,但往往难以做到滴水不漏。这时,浏览器提供的一项重要安全特性——Content Security Policy (CSP)——就显得尤为关键。那么今天,让我们先从理论层面好好来梳理一下 CSP,看看它到底有哪些约束来保障我们的 web 内容的安全性,下一期我们再从代码演示上来分别展示。
CSP 本质上是建立一个内容安全白名单,通过 HTTP 头或 标签告知浏览器哪些外部资源(脚本、样式、图片、字体、框架等)允许被加载和执行。如果资源的来源不在白名单内,浏览器就会拒绝加载,从而有效地阻止了大部分 XSS 攻击以及其他类型的内容注入攻击。
如下图,抖音官网的响应头中关于 CSP 相关的配置
想象一下,你的网站有一个留言板功能。如果没有对用户输入进行严格的过滤,攻击者可能会提交包含恶意 标签的留言。当其他用户浏览这条留言时,浏览器会解析并执行这个恶意脚本,造成 XSS 攻击。
传统的防御: 对用户输入进行 HTML 转义,例如将 <
转换为 <
。但这依赖于开发者对所有潜在输入点的严格控制,容易遗漏。
CSP 的防御: 通过设置 CSP,你可以限制哪些域名下的脚本可以执行,甚至完全禁止页面执行内联脚本() 或内联样式 (
,
style="..."
)。 极不推荐使用,因为它会引入 XSS 风险。
'unsafe-eval'
: 允许使用 eval()
, setTimeout("...")
, setInterval("...")
等从字符串执行代码的方法。 极不推荐使用,因为它也引入 XSS 风险。'nonce-'
: 允许带有特定 nonce 属性的内联脚本或样式。服务器在每次响应时生成一个唯一的 nonce 值,并将其添加到 CSP 头和相应的
或
标签上。这是一种相对安全的允许内联代码的方式,因为它需要攻击者知道当前的 nonce 值(难以猜测)。
script-src 'nonce-abc123xyz'
'sha256-'
: 允许具有特定哈希值的内联脚本或样式。你可以计算内联脚本或样式的哈希值(支持 SHA256, SHA384, SHA512),并将其添加到 CSP 头中。
script-src 'sha256-abcdefg...'
(你需要计算 alert('Hello, CSP!');
这段代码的 SHA256 哈希值)'strict-dynamic'
: 一个强大的指令(通常与 nonce 或 hash 一起使用)。如果一个脚本通过 nonce 或 hash 被允许执行,那么这个脚本加载的其他脚本(例如通过 document.createElement('script')
创建并添加到 DOM 中)也会被信任并允许执行,而无需显式地在 CSP 中列出这些脚本的来源。这极大地简化了对复杂应用(如 SPA)的 CSP 管理,同时保持了安全性。
script-src 'nonce-abc123xyz' 'strict-dynamic'
(即使 example.com
不在 script-src
中,dynamic.js
也会被允许加载)下面我们将创建一个简单的 Express 应用来演示如何设置 CSP 头部以及不同策略的效果。
项目结构:
csp-demo/
├── package.json
├── app.js
└── public/
├── index.html
├── safe-script.js
1. 初始化项目并安装 Express 和 body-parser (用于接收报告):
mkdir csp-demo
cd csp-demo
npm init -y
npm install express body-parser
2. 创建 public/index.html
:
CSP 示例
内容安全策略(CSP)演示页面
打开浏览器开发者工具查看 CSP 违规报告。
3. 创建 public/safe-script.js
:
console.log('这是一个来自同源的安全脚本。');
4. 创建 app.js
:
const express = require('express');
const path = require('path');
const bodyParser = require('body-parser');
const app = express();
const port = 3000;
// 使用 body-parser 中间件来解析请求体,用于处理 CSP 报告
// 针对 JSON 类型和 application/csp-report 类型的请求体进行解析
app.use(bodyParser.json({
type: ['json', 'application/csp-report'] // 特别用于处理 CSP 的违规报告
}));
// 自定义中间件:设置 Content-Security-Policy(内容安全策略)响应头
app.use((req, res, next) => {
// 定义你的 CSP 策略
// 当前策略规定:
// - 默认资源加载来源为 'self'(即同源)
// - 脚本允许从 'self' 和 cdnjs.cloudflare.com 加载
// - 样式表允许从 'self' 加载,并允许内联样式('unsafe-inline'),仅用于演示,生产环境应避免使用
// - 图片资源允许从 'self' 和 https://via.placeholder.com 加载
// - 所有违反策略的行为将报告到 /report 接口
const cspPolicy = `
default-src 'self';
script-src 'self' https://cdnjs.cloudflare.com 'sha256-WnZRYRws9lJmeyKcnwV8cR+ycNmLoVQQPANm6GNlsUk=';
style-src 'self' 'unsafe-inline';
img-src 'self' https://placehold.co;
report-uri /report;
`.replace(/\s+/g, ' ').trim(); // 清理多余的空白字符
// 使用 Content-Security-Policy 响应头来 **强制执行** 策略
res.setHeader('Content-Security-Policy', cspPolicy);
next();
});
// 从 'public' 目录提供静态文件(HTML、CSS、JS 等)
app.use(express.static(path.join(__dirname, 'public')));
// 接收 CSP 违规报告的接口
app.post('/report', (req, res) => {
console.log('收到 CSP 违规报告:');
console.log(JSON.stringify(req.body, null, 2)); // 将报告内容格式化后打印到控制台
res.sendStatus(204); // 返回 204 No Content 表示成功接收但无内容返回
});
// 启动服务器
app.listen(port, () => {
console.log(`CSP 示例应用正在运行在 http://localhost:${port}`);
console.log('请在浏览器中打开 http://localhost:3000 并查看控制台输出。');
});
5. 运行应用:
node app.js
打开浏览器访问 http://localhost:3000
。打开开发者工具的 Console (控制台) 和 Network (网络) 面板。
观察现象:
script-src 'self' https://cdnjs.cloudflare.com;
策略:
内联脚本("这是一个内联脚本.")会被阻止,除非策略中包含 'unsafe-inline'
或使用了 nonce/hash。
同源脚本 (safe-script.js
) 会被允许加载。
来自 cdnjs 的 jQuery 脚本会被允许加载。
console.log('这是一个由 hash 判断的脚本。'); 会被允许加载
style-src 'self' 'unsafe-inline';
策略:
body { ... }
) 会被允许,因为包含了 'unsafe-inline'
。如果去掉 'unsafe-inline'
,内联样式就会被阻止。img-src 'self' https://via.placeholder.com;
策略:
https://placehold.co
的图片会被允许加载。如果去掉这个来源,图片就会被阻止。onclick="alert('...')
) 通常会被 script-src
阻止,即使有 'unsafe-inline'
。它们需要更宽松的策略或使用其他技术(如通过 JavaScript 添加事件监听器)。report-uri /report;
并且有违规发生,服务器端的控制台会打印出接收到的违规报告的 JSON 数据。你可以修改 app.js
中的 cspPolicy
来实验不同的 CSP 策略,观察它们对页面行为的影响。例如,去掉 'unsafe-inline'
,看看内联脚本和样式是否被阻止。
现代 Web 开发通常使用构建工具(如 Webpack 或 Vite)来打包、优化和处理资源。构建过程可能会产生一些 CSP 需要特别关注的问题:
bundle.abcdef.js
)。CSP 策略通常是基于来源路径,这与哈希文件名不冲突。解决方案:
script-src
和/或 style-src
指令中,例如 script-src 'nonce-YOUR_NONCE_VALUE' 'self' ...;
。nonce
属性添加到构建工具生成的内联
或
标签上。script-src
和/或 style-src
指令中,例如 script-src 'sha256-HASH_OF_SCRIPT1' 'sha256-HASH_OF_SCRIPT2' 'self' ...;
。'strict-dynamic'
(与 Nonce/Hash 配合): 当使用 nonce
或 hash
允许了初始脚本后,可以使用 'strict-dynamic'
允许这些被信任的脚本动态加载其他脚本,而无需在 CSP 中明确列出所有可能的来源。这极大地简化了 SPA 等应用的 CSP 配置。
script-src 'nonce-YOUR_NONCE_VALUE' 'strict-dynamic' 'self' https:; object-src 'none'; base-uri 'self';
'strict-dynamic'
生效时,会忽略 'unsafe-inline'
和基于 URL 的白名单(例如 https://cdn.example.com
),除非它们也带有 Nonce 或 Hash 或通过被信任的脚本加载。但 'self'
通常仍然有效作为回退。eval()
。report-only
开始: 在生产环境中使用 Content-Security-Policy-Report-Only
头部和 report-uri
指令。部署策略,并收集违规报告,分析哪些合法的资源被阻止了。'unsafe-inline'
和 'unsafe-eval'
,明确列出允许的来源。优先解决 script-src
和 default-src
的问题。'strict-dynamic'
简化管理。Content-Security-Policy-Report-Only
切换为 Content-Security-Policy
。report-uri
或 report-to
收集报告,以便及时发现新的问题或潜在的攻击尝试。Content Security Policy (CSP) 是现代 Web 安全中不可或缺的一部分,它是防御 XSS 和其他内容注入攻击的强大武器。通过明确声明页面允许加载和执行的资源来源,CSP 为浏览器提供了一个安全基线,即使其他防御措施失效,也能在一定程度上限制攻击的危害范围。
虽然部署 CSP 需要一些工作来分析现有应用并调整策略,但这项投入是值得的。从 report-only
模式开始,逐步收紧策略,结合 Nonce 或 Hash 处理内联代码,并利用 'strict-dynamic'
简化复杂应用的策略管理,你就能有效地提升你的 Web 应用的安全性,为你的用户提供更安全的浏览体验。