Content-Security-Policy(CSP)是一个响应头,告诉浏览器页面可以使用哪些脚本、样式、图片、框架和连接。如果攻击者设法把脚本塞进你的 HTML,一份好的策略能让浏览器拒绝执行它。从论坛帖子里照搬的策略通常会走向两个极端:要么弄坏统计代码、网页字体和支付组件,要么被塞满 'unsafe-inline' 和通配符,最后几乎什么都放行。
本文为三类网站提供可用的示例,解释什么让策略变得严格(nonce、hash 和 'strict-dynamic'),介绍 default-src 覆盖不到的指令,演示如何收集违规报告,并给出从 Report-Only 模式开始的上线流程,避免在毫无预警的情况下弄坏页面。所有示例都使用 example.com,请替换为你自己的值。
本指南的适用范围
免费检测工具只读取一次公开响应的响应头:即你输入的源站首页(最多跟随五次重定向)。它只读取已生效的 Content-Security-Policy 头,不读取 Report-Only 策略,也不判断策略是否适合你的网站,不属于渗透测试。
1. Content-Security-Policy 的作用与局限
策略是由分号分隔的一组指令。每条指令指定一种资源类型及其允许的来源:script-src 管 JavaScript,style-src 管 CSS,另有 img-src、font-src,管 fetch、XHR 和 WebSocket 连接的 connect-src,以及管页面嵌入框架的 frame-src。当某条具体指令缺失时,default-src 是这些 fetch 类指令的后备(W3C CSP Level 3)。
有三条重要指令不会回退到 default-src:frame-ancestors(哪些站点可以嵌入你的页面)、base-uri(<base> 元素可以设置什么)和 form-action(表单可以提交到哪里)。因此,仅有 default-src 'self' 的策略仍然允许任何站点嵌入页面,也允许被注入的 <base> 标签改写相对 URL。
来源值分为三类:
- 关键字,用单引号括起:
'self'(页面自身的源)、'none'(什么都不允许)、'unsafe-inline'、'unsafe-eval'和'strict-dynamic'。 - 主机和协议:
https://cdn.example.com、https://*.example.com、https:或data:。 - Nonce 和 hash:
'nonce-…'和'sha256-…',它们只放行单个脚本或样式元素,而不是整个主机。
请通过 HTTP 响应头发送策略。<meta http-equiv="Content-Security-Policy"> 标签对大多数指令有效,但规范规定其中不支持 frame-ancestors、report-uri 和 sandbox,而 Report-Only 策略根本无法通过 meta 标签下发(MDN CSP 指南)。请保持合理预期:CSP 限制的是被注入代码能做什么,它不能取代对输出的转义和对输入的校验,后者才是从源头阻止注入的手段。
2. 三个可作为起点的 Content-Security-Policy 示例
示例 A:不含第三方脚本的网站。当所有脚本和样式表都是你自己源站上的文件时,白名单策略就够用:
Content-Security-Policy: default-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'; upgrade-insecure-requests它会拦截所有内联 <script>、所有 style 属性以及从其他主机加载的内容,因此适合没有嵌入内容的手工搭建或静态生成的网站。
示例 B:使用 nonce 的服务器端渲染网站。这是 web.dev 为服务器逐请求生成的页面推荐的严格策略,并加上了防嵌套保护:
Content-Security-Policy: script-src 'nonce-R4nd0mBase64Value' 'strict-dynamic'; object-src 'none'; base-uri 'none'; frame-ancestors 'self'nonce 在每次响应中都会变化(见第 3 节)。注意它省略了什么:没有 default-src,样式、图片、字体和连接都不受限制。这是有意的取舍:它把重点放在脚本执行上(跨站脚本的大部分危害都出在这里),同时避免了供应商更换域名就会失效的主机列表。
示例 C:使用 hash 的静态或缓存页面。当同一份 HTML 提供给所有人时,改用 hash 放行内联脚本:
Content-Security-Policy: script-src 'sha256-BASE64_HASH_OF_INLINE_LOADER' 'strict-dynamic'; object-src 'none'; base-uri 'none'; frame-ancestors 'self'| 策略类型 | 适用场景 | 持续工作 | 注意事项 |
|---|---|---|---|
| A:主机白名单 | 没有第三方代码的静态网站 | 新增供应商时更新主机列表 | 允许主机上的任何脚本都能被加载,包括共享 CDN 上的旧版本库 |
| B:nonce + strict-dynamic | 由应用逐请求渲染的页面 | 在模板中为每个 script 标签添加 nonce | 页面缓存会把同一个 nonce 重放给所有访客 |
| C:hash + strict-dynamic | 静态、预渲染或边缘缓存的 HTML | 内联代码变化时重新计算 hash | 压缩工具和模板改变空白字符,从而改变 hash |
无论选哪一种,都请加上第 6 节的报告指令,并先以 Content-Security-Policy-Report-Only 发送。
3. 用 nonce 和 hash 取代 'unsafe-inline'
'unsafe-inline' 会放行所有内联脚本,包括攻击者注入的脚本,这就抵消了 CSP 针对跨站脚本的大部分防护。Nonce 和 hash 则只放行你本来就打算发布的内联代码。当同一指令中包含 nonce 或 hash 时,浏览器会忽略 'unsafe-inline'(MDN),因此严格策略中可以保留它,作为非常老旧浏览器的后备。
Nonce
nonce 是服务器为每次响应生成的随机值,写入响应头,并复制到每个要运行的脚本的 nonce 属性中。它必须不可预测,且每次响应都要更新;web.dev 建议使用加密安全的生成器产生至少 128 位。在 Node.js 应用中:
import crypto from 'node:crypto';
app.use((req, res, next) => {
const nonce = crypto.randomBytes(16).toString('base64');
res.locals.nonce = nonce;
res.setHeader('Content-Security-Policy',
`script-src 'nonce-${nonce}' 'strict-dynamic'; object-src 'none'; base-uri 'none'; frame-ancestors 'self'`);
next();
});模板随后以相同的值写出 <script nonce="…" src="/app.js"></script>。请在模板中写入你自己脚本标签的位置添加 nonce,不要对最终生成的 HTML 中的每个 <script> 统一改写:一刀切的改写会连攻击者注入的脚本也一并放行。有两个常见陷阱:缓存 HTML 的 CDN 或页面缓存会把同一个 nonce 重放给所有访客;在构建时写死到静态文件里的值根本不是 nonce。
Hash
hash 放行的是某一段确切的内联脚本:即 script 标签之间文本的 base64 编码 SHA-256、SHA-384 或 SHA-512 摘要。每个字符都算数,包括空格和换行,因此压缩工具或模板一改,hash 就会变。这使得 hash 更适合静态和缓存页面,因为每次响应的内容都相同。Chrome 在拦截内联脚本时会在控制台打印期望的 hash,你也可以自己计算:
printf '%s' 'console.log("hello")' | openssl dgst -sha256 -binary | openssl base64两者都不放行的内容
基于 nonce 或 hash 的策略会拦截 onclick="…" 这类内联事件处理器和 javascript: URL。请把这些代码移到脚本文件中,并用 addEventListener 绑定。'unsafe-hashes' 可以按 hash 放行某个特定处理器,但只应视为权宜之计。把字符串当代码执行的写法,例如 eval()、new Function() 或带字符串参数的 setTimeout,需要 'unsafe-eval';能替换就替换,如果只是 WebAssembly 需要编译,可以使用范围更窄的 'wasm-unsafe-eval'(MDN script-src)。
4. strict-dynamic:让受信任的脚本加载它所需的内容
大多数网站运行的脚本会再加载更多脚本:标签管理器添加统计代码,同意横幅添加其供应商脚本,聊天组件拉取自己的包。把它们可能用到的所有主机都列出来很脆弱。'strict-dynamic' 走了另一条路:被 nonce 或 hash 放行的脚本可以再添加其他脚本,这些脚本同样被信任。
在支持它的浏览器中,'strict-dynamic' 还会让浏览器忽略 script-src 中的主机白名单、'self' 和 'unsafe-inline'(MDN)。由此产生三个后果:
- HTML 中的每个
<script>标签都需要 nonce 或匹配的 hash,包括来自你自己源站的脚本,因为'self'不再放行它们。 - 受信任脚本通过
document.createElement('script')创建的脚本会被允许;通过document.write写入页面的脚本(解析器插入的脚本)则不会,因此依赖它的旧版嵌入代码会失效。 - 信任会向下传递。标签管理器加载的任何内容都会运行,所以所有能在标签管理器中发布内容的人,实际上都能在你的网站上运行代码。请像对待部署权限一样对待这项权限。
对于较旧的浏览器,可以添加新浏览器会忽略的后备值:
script-src 'nonce-R4nd0mBase64Value' 'strict-dynamic' https: 'unsafe-inline'; object-src 'none'; base-uri 'none'支持 CSP Level 3 的浏览器只应用 nonce 和 'strict-dynamic'。认识 nonce 但不认识 'strict-dynamic' 的浏览器使用 nonce 和 https:,非常老的浏览器则回退到 'unsafe-inline' https:。Google 提供了支持 nonce 的 Tag Manager 代码片段,会把 nonce 传递给它添加的脚本,并指出 Tag Manager 中的自定义 JavaScript 变量仍然需要 'unsafe-eval'(Tag Manager CSP 指南)。
5. frame-ancestors、object-src 'none' 和 base-uri:需要显式添加的指令
frame-ancestors决定哪些站点可以用框架嵌入你的页面,是防御点击劫持的手段。如果没有人需要嵌入该页面,使用'none';只有自己的页面嵌入时使用'self';也可以列出确切的源,例如https://app.example.com。它只在 HTTP 响应头中有效。X-Frame-Options(DENY或SAMEORIGIN)仍可作为旧浏览器的后备;请让两者允许相同的嵌入范围(MDN frame-ancestors)。object-src 'none'拦截<object>和<embed>。现代浏览器已不再支持插件,但这些元素仍可加载内容,而没有default-src的严格策略如果不明确写出,就不会限制它们。base-uri 'none'(如果页面使用了<base>元素,则用'self')可以阻止被注入的<base href>把所有相对脚本 URL 指向其他主机。form-action 'self'限制表单可以提交到的位置。如果表单会提交给支付或登录服务商,请加上对方的源,并在变更后测试这些流程。upgrade-insecure-requests让浏览器通过 HTTPS 获取http://子资源。它有助于解决混合内容问题,但不能取代 HSTS。
请把这些指令显式写入每一份策略,包括上面的示例 B 和 C。它们几乎不需要维护,却能补上脚本规则留下的缺口。
6. 使用 report-to 和 report-uri 上报违规
违规报告能告诉你,在真实访客的浏览器里,策略拦截了什么,或者在 Report-Only 模式下会拦截什么。CSP Level 3 定义了 report-to,它指向 Reporting-Endpoints 响应头中声明的端点,同时弃用了直接接收 URL 的旧指令 report-uri。MDN 将 report-to 标为自 2026 年起在当前主流浏览器中新近可用,而较旧版本的浏览器只认 report-uri。支持 report-to 的浏览器会忽略 report-uri,所以目前请两者都发送(MDN report-to):
Reporting-Endpoints: csp="https://example.com/csp-reports"
Content-Security-Policy-Report-Only: script-src 'nonce-R4nd0mBase64Value' 'strict-dynamic' 'report-sample'; object-src 'none'; base-uri 'none'; report-uri https://example.com/csp-reports; report-to csp两种机制提交的格式不同。report-uri 发送带有 csp-report 键的 JSON 对象,内容类型为 application/csp-report。report-to 发送 application/reports+json:这是一个报告数组,其中 type 为 csp-violation,正文包含 documentURL、blockedURL、effectiveDirective 和 disposition(enforce 或 report)等字段。启用 'report-sample' 后,针对被拦截内联代码的报告会包含其前 40 个字符。请让同一个端点同时接受两种格式。
- 预期会有噪声。浏览器扩展会注入脚本,并触发来源文件为
chrome-extension://或moz-extension://的违规。请按指令和被拦截主机对报告分组,寻找规律,而不是盯着单条记录。 - 把报告当作个人数据处理。文档 URL 和 referrer 可能带有包含邮箱地址或令牌的查询字符串。请去除或截短它们,只在较短时间内保留报告,并且无故不要转交他人。
- 保护端点。任何人都可以提交伪造报告,因此请设置大小和频率限制,并且绝不要在管理后台中不经转义地显示报告内容。
7. 常见的故障点:统计代码、字体、内联脚本和嵌入内容
头几天出现的大多数违规来自少数几个来源。报告中的 effectiveDirective 会告诉你该查看哪条规则:
| 现象 | 报告中的指令 | 典型修复 |
|---|---|---|
| 统计代码不再记录页面浏览 | script-src-elem、connect-src、img-src | 用 nonce 加载该标签,或放行其脚本主机,并放行供应商文档中列出的数据收集主机(Google 按产品列出了这些主机) |
| 网页字体回退为系统字体 | font-src、style-src-elem | 在 style-src 中放行样式表主机,在 font-src 中放行字体文件主机(Google Fonts 对应 fonts.googleapis.com 和 fonts.gstatic.com),或者自行托管这些文件 |
| 主题或 CMS 代码片段失效 | script-src-elem,且 blockedURL 为“inline” | 在模板中添加 nonce,把代码移到文件中,或放行其 hash |
带 onclick 的按钮没有反应 | script-src-attr | 在脚本文件中用 addEventListener 重写处理器 |
| 标签管理器的自定义变量没有返回值 | script-src(eval) | 改用内置变量,或把 'unsafe-eval' 作为有记录的例外接受下来 |
| 嵌入的视频、地图或支付框架一片空白 | frame-src | 添加服务商的确切源 |
| 内联样式被忽略,布局发生偏移 | style-src-elem、style-src-attr | 把样式移到样式表中,为 style 元素添加 nonce,或仅在 style-src 中保留 'unsafe-inline',并作为有记录的例外 |
| 聊天或实时更新无法连接 | connect-src | 添加供应商的 https:// 和 wss:// 主机 |
| 你的应用或合作方无法再嵌入该页面 | frame-ancestors | 列出嵌入该页面的确切源 |
内联样式的风险小于内联脚本,但并非无害:被注入的 CSS 可以改变访客看到的内容,借助属性选择器还可能泄露部分页面数据。许多框架和 CSS-in-JS 库会在运行时插入 style 元素,所以在从 style-src 中移除 'unsafe-inline' 之前,请先确认你使用的库是否支持 nonce。自行托管字体和库可以一次性消除上表中的好几行问题,并让策略保持简短。
8. 分阶段上线 CSP
- 盘点。列出关键页面上的脚本、样式、字体、框架和连接目标:首页、登录、账户、结账以及带嵌入组件的页面。DevTools 的 Network 面板和标签管理器配置是最快的信息来源。
- Report-Only。以
Content-Security-Policy-Report-Only发送草案并启用报告。不会拦截任何内容;违规会出现在控制台和你的端点上。让它运行足够长的时间,使访问较少的页面和营销活动页面也能出现,通常需要一周或更久。 - 修复来源,而不是放宽策略。添加 nonce,把内联处理器移到文件中,自行托管字体。只有在有负责人和理由并做了记录的前提下,才添加主机或
'unsafe-eval'作为例外。 - 强制执行。把响应头名称改为
Content-Security-Policy。同时保留一份更严格的 Report-Only 策略并行运行,用来测试下一步收紧:当两个响应头同时到达时,浏览器会强制执行其中一个,对另一个只上报。 - 持续观察。新的营销标签、插件更新或框架升级都可能带来新的内联代码。请保持报告端点运行,并在每次服务器、CDN 或框架变更后再次检查响应头。
有两个基础设施细节常常带来意外。如果 CDN 或框架添加了自己的 CSP 头,浏览器会执行收到的每一份策略,资源必须通过所有策略,所以额外的响应头只会让限制更严,绝不会更松(MDN)。另外,如果 HTML 在边缘被缓存,基于 nonce 的策略需要在页面组装的位置生成 nonce,或者改用 hash。要查看页面实际发送了什么:
curl -sS -D - -o /dev/null https://example.com/ | grep -i content-security-policy9. 用免费的安全响应头检测工具检查已部署的响应头
策略生效后,免费的安全响应头检测工具可以展示浏览器从你的首页收到了什么。它运行的是免费的上线准备快照,对一个公开域名执行八项被动检查,无需注册,并把响应头检查排在其余七项之前展示。
- 它请求你输入的源站首页,最多跟随五次重定向,并读取最终响应。登录或结账等其他页面通常带有不同的策略,请用 DevTools 或 curl 检查。
- 它只读取已生效的
Content-Security-Policy头。在 Report-Only 阶段,它仍会把 CSP 报告为缺失,风险等级为中,因为仅报告的策略不拦截任何内容。在你正式强制执行之前,这个结果是正常的。 - 已生效策略中的
frame-ancestors指令与X-Frame-Options一样,算作防嵌套保护。 - 在单独的加固评级(A 到 F)中,当
default-src或script-src、style-src指令(包括-elem和-attr变体)含有'unsafe-inline',或当default-src或脚本指令含有'unsafe-eval'时,CSP 得到的分数会更低。评级按策略的字面内容判断,所以 nonce 旁边的'unsafe-inline'后备也会降低得分,尽管当前浏览器会忽略它。 - 它不评估 nonce、hash、
'strict-dynamic'、object-src、base-uri或报告配置。这些请使用浏览器控制台和你的违规报告来检查。
结果会显示总分以及八项检查各自的结果,让 CSP 信号与 HSTS、TLS 和其他公开检查并列呈现。
常见问题
从哪个 Content-Security-Policy 示例开始比较好?
对于服务器端渲染的网站:script-src 'nonce-{random}' 'strict-dynamic'; object-src 'none'; base-uri 'none'; frame-ancestors 'self',并且每次响应都使用新的 nonce。对于静态或缓存页面,用内联脚本的 hash 代替 nonce。两种方案都先以 Content-Security-Policy-Report-Only 启动。
CSP 中应该用 nonce 还是 hash?
当应用逐页构建时用 nonce,因为每次都可以插入新值。当 HTML 是静态或被缓存时用 hash,因为缓存的 nonce 会被重复发给每位访客。两者都可以与 'strict-dynamic' 配合使用。
style-src 中的 'unsafe-inline' 是问题吗?
它的风险小于 script-src 中的 'unsafe-inline',但被注入的 CSS 仍可能改变访客看到的内容并泄露部分页面数据。仅在把样式迁入样式表或为 style 元素添加 nonce 的过渡期内,将其作为有记录的例外保留。
Content-Security-Policy-Report-Only 能保护我的网站吗?
不能。它不拦截任何内容,只报告强制执行的策略会拦截什么。请用它做测试,然后切换到 Content-Security-Policy 头。两个头可以同时发送,这对测试下一版更严格的策略很有用。
可以通过 meta 标签设置 CSP 吗?
大多数指令可以。但 frame-ancestors、report-uri 和 sandbox 在 meta 标签中会被忽略,Report-Only 策略也无法通过它下发。只要服务器或托管平台允许,就请使用 HTTP 响应头。
report-uri 和 report-to 有什么区别?
report-uri 直接接收 URL;CSP Level 3 已弃用它,但较旧的浏览器仍依赖它。report-to 指向 Reporting-Endpoints 响应头中的端点,并使用 Reporting API 格式。支持 report-to 的浏览器会忽略 report-uri,所以两者同时发送没有问题。
来源与延伸阅读
- W3C:Content Security Policy Level 3www.w3.org
- MDN:内容安全策略(CSP)指南developer.mozilla.org
- MDN:Content-Security-Policy 响应头developer.mozilla.org
- MDN:CSP script-src 指令developer.mozilla.org
- MDN:CSP frame-ancestors 指令developer.mozilla.org
- MDN:CSP report-to 指令developer.mozilla.org
- MDN:Reporting-Endpoints 响应头developer.mozilla.org
- W3C:Reporting APIwww.w3.org
- web.dev:用严格的内容安全策略(CSP)缓解跨站脚本(XSS)web.dev
- Google Tag Platform:在内容安全策略下使用 Tag Managerdevelopers.google.com
- OWASP 内容安全策略速查表cheatsheetseries.owasp.org
由 Sitelemetry 编辑团队编写。文末的一手资料可帮助你深入了解相关主题。



