这是一篇面向有实践需求的前端工程师与架构师的长文,以
CSP的设计目标、语义、指令组合与落地策略为主线,穿插真实事故与可运行配置,帮你把CSP从概念变成可上线的策略。
1. CSP 是什么:浏览器可执行权限的白名单机制
CSP 全称 Content Security Policy,是浏览器支持的安全标准,站点用一个响应头把页面允许执行或加载的外部资源来源、执行方式、是否可被嵌入等规则明确告诉浏览器。浏览器据此对白名单之外的行为做拒绝或仅上报。在对抗 XSS、点击劫持与部分供应链脚本入侵方面,CSP 是最有效的客户端防线之一。MDN 的定义强调:核心是让服务器声明哪些来源可以被加载与执行,以及如何执行。(MDN Web Docs)
从标准层面看,CSP 已发展到 Level 3,引入了 strict-dynamic、unsafe-hashes 等现代能力,并与 Trusted Types 协作收敛 DOM XSS 的注入面。(w3.org)
2. CSP 的基本工作流:策略声明、强制或仅上报、浏览器执行
页面返回头里带上策略:
- 强制执行:
Content-Security-Policy - 仅上报:
Content-Security-Policy-Report-Only
仅上报模式常用于灰度或观察期,便于统计违规点并逐步收紧,而不影响线上功能。MDN 还说明若使用 report-to,需先用 Reporting-Endpoints 声明端点名称与地址。(MDN Web Docs)
3. 你需要掌握的高频指令与语义要点
为了避免策略写成看起来很安全但实际无效的样子,我们把高频指令与实战语义拆开讲。
3.1 default-src 与资源族指令
default-src是兜底来源,未单独配置的资源族会继承它。- 常见族包括
script-src、style-src、img-src、connect-src、font-src、frame-src等。(MDN Web Docs)
3.2 script-src 的现代写法
- 用
nonce-xxxx或sha256-...哈希给内联脚本授信,杜绝unsafe-inline。 - 配合
strict-dynamic:对带nonce或hash的根脚本授信,其动态创建的脚本也继承信任,同时忽略宽泛的允许列表,比如self。这能极大简化域名白名单维护,收敛供应链风险。(MDN Web Docs)
MDN明确指出strict-dynamic会把信任从带nonce的根脚本传递到它加载的脚本上,并在该模式下忽略传统的来源白名单。(MDN Web Docs)
3.3 frame-ancestors 与点击劫持
- 控制谁可以把你的站点用
iframe等嵌入。想完全禁止可设为'none'。相较过时的X-Frame-Options,应优先使用frame-ancestors。(MDN Web Docs)
3.4 混合内容与迁移到 HTTPS
upgrade-insecure-requests指示浏览器把同站或子资源的HTTP链接自动升级为HTTPS,用于遗留链接多的场景。block-all-mixed-content已被标记为不推荐,规范层面也说明其已过时,推荐用自动升级与现代混合内容策略替代。(MDN Web Docs)
3.5 上报与可观测性
- 现代做法使用
report-to,并通过Reporting-Endpoints定义端点;老的report-uri已在文档中标注为弃用。(MDN Web Docs)
3.6 与 Trusted Types 协作
- 在策略中加入
require-trusted-types-for 'script',让诸如innerHTML这类DOM XSS注入点只接受Trusted Types值,从语言层面拒绝字符串写入。 - 再配合
trusted-types指令限制允许创建哪些策略。MDN对两者有系统解释。(MDN Web Docs)
4. 真实世界的案例:Magecart 与大厂的 CSP 实践
电商场景里,Magecart 家族通过在结算页注入恶意脚本来窃取信用卡数据。British Airways 的 2018 年事件是业界常被复盘的案例:攻击者把一段特制的 JavaScript 植入航司网站与 Android 应用共享的网页组件中,造成大量交易数据被窃。安全研究给出细节分析,并指出 CSP 可以作为缓解手段之一,用以限制外联与脚本来源。(WIRED)
业界文章也分析了 BA、Newegg、Ticketmaster 等事件,指出若使用严格的 CSP(包含脚本哈希或 nonce、限制网络外联域名),可以显著降低外部篡改脚本落地与数据外泄的成功率;当然,CSP 对被允许域名自身被入侵的场景作用有限,这也是它作为深度防御的一部分而非唯一手段的原因。(bluetriangle.com)
另一方面,GitHub 多年前就公开其 CSP 实施经历,称其为对抗内容注入最有效的缓解之一,这说明在复杂前端体系里,CSP 依然可行且收益显著。(The GitHub Blog)
5. 从纸面策略到可上线配置:三种常见落地路径
5.1 Node.js Express 中设置严格 CSP
// 注意:示例仅演示思路。真实项目应为每个请求生成随机 nonce,并注入到模板里。
// 此处不使用英文双引号。
import express from 'express';
import crypto from 'node:crypto';
const app = express();
app.use((req, res, next) => {
const nonce = crypto.randomBytes(16).toString('base64'); // 供模板内联脚本使用
res.locals.cspNonce = nonce;
// 使用 strict-dynamic,允许携带该 nonce 的根脚本动态加载下游脚本
const csp = [
`default-src 'self'`,
`base-uri 'self'`,
`object-src 'none'`,
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
`style-src 'self' 'unsafe-inline'`, // 若能用哈希更好
`img-src 'self' data:`,
`font-src 'self'`,
`connect-src 'self' https://api.example.com`,
`frame-ancestors 'none'`,
`upgrade-insecure-requests`,
`report-to csp-endpoint`,
].join('; ');
res.setHeader('Content-Security-Policy', csp);
res.setHeader('Reporting-Endpoints', `csp-endpoint=https://report.example.com/csp`);
next();
});
app.get('/', (req, res) => {
// 模板里把 res.locals.cspNonce 写到内联脚本的 nonce 属性
res.send(`
<!doctype html>
<meta charset=utf-8>
<title>CSP Demo</title>
<script nonce=${res.locals.cspNonce}>
// 受信任的内联脚本
console.log('csp ok');
</script>
`);
});
app.listen(3000);
上面示例体现了几条关键做法:用 nonce 配合 strict-dynamic 管控脚本信任链、用 frame-ancestors 防点击劫持、用 upgrade-insecure-requests 帮助遗留资源升级、用 report-to 上报违规。对应语义与可用性详见 MDN 与规范。(MDN Web Docs)
5.2 Nginx 网关侧统一下发
# server 块内
add_header Content-Security-Policy "default-src 'self'; base-uri 'self'; object-src 'none'; script-src 'self' 'nonce-$request_id' 'strict-dynamic'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self' https://api.example.com; frame-ancestors 'none'; upgrade-insecure-requests; report-to csp-endpoint" always;
add_header Reporting-Endpoints "csp-endpoint=https://report.example.com/csp" always;
网关侧统一策略能快速覆盖多服务,但要解决 nonce 与模板的对接。可以通过上游注入变量或只允许外链脚本并给出哈希来落地。语义参考 MDN 的头字段文档。(MDN Web Docs)
5.3 静态托管场景用 meta 标签
当你用 GitHub Pages 一类静态托管,无法改响应头,可以考虑在 head 里用 meta 形式声明 CSP(注意某些指令如 report-uri 的兼容性限制)。(site)
<meta http-equiv=Content-Security-Policy content="default-src 'self'; img-src 'self' data:; object-src 'none'; frame-ancestors 'none'">
6. 迁移与收紧的操作手册:避免一刀切造成线上事故
一套合理的节奏能让 CSP 从可观测演进到强制:
- 先上
Report-Only。把当前页面真实的外联、内联习惯暴露出来,接入report-to收集数据。(MDN Web Docs) - 清点脚本与样式来源。给确需的第三方脚本加
哈希或通过受信任加载器注入nonce。strict-dynamic可以减少域名白名单维护量。(MDN Web Docs) - 把
frame-ancestors从宽松改为精确域或'none',替换掉老旧的X-Frame-Options。(MDN Web Docs) - 开启
upgrade-insecure-requests,逐步清理遗留HTTP资源;避免使用已不推荐的block-all-mixed-content。(MDN Web Docs) - 对富交互应用,评估开启
Trusted Types的成本与收益,在关键页面用require-trusted-types-for 'script'限制危险DOM接口的字符串输入。(MDN Web Docs)
7. 实战策略配方:从合规到进攻性防御
7.1 电商结账页防数据外泄
目标是防止外部脚本加载与把数据发往未知 endpoint。
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{per-request-nonce}' 'strict-dynamic';
connect-src 'self' https://pay.example.com https://cdn.payments.example;
img-src 'self' data:;
style-src 'self' 'unsafe-inline';
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
form-action 'self' https://pay.example.com;
upgrade-insecure-requests;
report-to csp-endpoint;
解释:用 strict-dynamic 把信任限定在携带 nonce 的根脚本及其子孙,connect-src 白名单只允许支付相关域,form-action 约束表单提交目标,frame-ancestors 禁止被套 iframe。这些组合针对 Magecart 类入侵特别有效,但若被允许的 CDN 自身被攻破,CSP 无法从根本阻止,此处需要供应链治理与前端完整性监控配合。(MDN Web Docs)
7.2 后台管理与内嵌集成
若页面需要被可信父页面内嵌(比如公司门户),可以把 frame-ancestors 指向精确的父域;若完全不需要嵌入,直接 'none'。OWASP 的点击劫持备忘清单也推荐用现代指令替代 X-Frame-Options。(MDN Web Docs)
7.3 渐进使用 Trusted Types
对使用大量 innerHTML 的存量项目,可以先在只上报模式下开启:
Content-Security-Policy-Report-Only:
require-trusted-types-for 'script';
trusted-types default myapp#html sanitize-html;
report-to csp-endpoint;
当违规上报趋近 0,再切换到强制模式。MDN 有关于把 Trusted Types 与 CSP 结合的示例与异常形态说明。(MDN Web Docs)
8. 常见坑位与避障指南
- 把
unsafe-inline设在script-src会几乎失去CSP的价值。应尽量用nonce或脚本sha256哈希替代。MDN的script-src文档详细列出各类来源表达式及其后果。(MDN Web Docs) - 重度依赖
eval的框架(或启用wasm动态编译)可能需要unsafe-eval或相关豁免,但这会扩大攻击面,应通过构建与运行时配置消除。可把严格策略放在敏感页优先上线。(MDN Web Docs) - 混合内容治理别依赖已弃用的
block-all-mixed-content;使用upgrade-insecure-requests更稳妥,并在W3C的混合内容规范中也有相应说明。(MDN Web Docs) - 报告端点需要容量与清洗策略,
report-to的可用性在不同浏览器存在差异,MDN明确其并非Baseline,部署前要做兼容性评估。(MDN Web Docs)
9. 观念升级:把 CSP 作为深度防御的合奏
OWASP 的备忘单把 CSP 定位为第二道防线:它无法阻止代码里存在漏洞,但能显著提高利用门槛。业界的安全团队通常会把它与输入校验、输出编码、模板沙箱、子资源完整性 SRI、第三方脚本监控与 WAF 联动。(OWASP Cheat Sheet Series)
与此相呼应,GitHub 的经验表明,即使在极其复杂的前端与扩展生态里,通过严格的策略、灰度与监测,CSP 依旧能带来可观的实战收益。(The GitHub Blog)
10. 一张可抄作业的生产级起步策略
下面这份策略可以作为大多数 SPA 的起点,按需替换域名与 nonce,并在 Report-Only 模式下跑一周,消化上报,再切换强制。
Content-Security-Policy:
default-src 'self';
base-uri 'self';
object-src 'none';
script-src 'self' 'nonce-{per-request-nonce}' 'strict-dynamic';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
font-src 'self';
connect-src 'self' https://api.example.com https://telemetry.example.com;
frame-src https://trusted-widgets.example.com;
frame-ancestors 'none';
form-action 'self';
upgrade-insecure-requests;
report-to csp-endpoint;
- 如果站点需要第三方
CDN的脚本,可给具体脚本加sha256-...哈希并移除strict-dynamic,或把第三方资源走你自家的可信加载器,通过nonce动态注入。 - 对于历史遗留大量内联样式的项目,
style-src的unsafe-inline在短期内很难去掉;建议引入构建期内联样式哈希化或逐步外链化,最终收紧到只允许哈希。具体迁移实践可参考MDN的实施文档。(MDN Web Docs)
11. CSP 报告聚合与调优闭环
- 使用
Content-Security-Policy-Report-Only与report-to,把违规事件集中到日志平台,打上页面、用户代理、来源等标签,按频率×影响排序修复。 - 若你的平台或
SaaS已上CSP,例如某些Microsoft 365组件与SharePoint Online正在分阶段推广CSP报告模式,可以参考其文档与社区讨论的实践细节。(GitHub)
12. 小结:CSP 的取舍与边界
CSP 不是银弹,却是把前端执行面收紧到你能审计的范围的关键手段。它能阻断大量直连外域与未受信任脚本的攻击路径,对 Magecart 这样的供应链注入尤为关键;它也有边界,比如被允许域名本身被攻陷时需要靠供应链治理与监控兜底。和大多数工程实践一样,CSP 需要一步步把现状拉到理想模型:先观测、再收紧、最后强制。
参考与延伸阅读
MDNContent-Security-Policy概览与头字段文档。(MDN Web Docs)MDNscript-src与strict-dynamic、实施指南。(MDN Web Docs)MDNframe-ancestors与点击劫持对抗;老X-Frame-Options的替代关系。(MDN Web Docs)- 混合内容的新旧策略:
upgrade-insecure-requests与block-all-mixed-content的弃用。(MDN Web Docs) Trusted Types与require-trusted-types-for。(MDN Web Docs)British Airways事件与Magecart攻击复盘。(WIRED)GitHub的CSP实施经验。(The GitHub Blog)OWASPCSP备忘单。(OWASP Cheat Sheet Series)
完
















