内容安全策略(CSP)是目前防御跨站脚本攻击(XSS)最有效的浏览器端机制之一,它的核心原理是通过HTTP响应头告诉浏览器哪些资源可以加载和执行,哪些必须被拦截。简单来说,你在服务器端配置一条CSP规则,浏览器就会严格按照这条规则来过滤页面中的脚本、样式、图片、字体等资源,任何不在白名单里的内容都会被直接阻断,从而让攻击者注入的恶意脚本根本没有执行的机会。部署CSP不需要改代码逻辑,只需要在Web服务器或应用层添加响应头即可,但要配好、配对,需要对网站的资源加载方式有清晰的了解。
什么是XSS攻击以及为什么CSP能防住它
跨站脚本攻击(XSS)的本质是攻击者把恶意JavaScript代码注入到网页中,当其他用户访问这个页面时,恶意脚本就会在受害者的浏览器里执行。这可以导致窃取Cookie、劫持会话、伪造操作、钓鱼跳转等严重后果。传统的防御手段比如输入过滤、输出编码,都是在服务端或前端做"清洗",但总有遗漏的地方。CSP的思路完全不同,它不去清洗内容,而是直接在浏览器层面设立一道"白名单墙"——只允许你明确授权的脚本运行,其他一切都不许动。这种"默认拒绝"的策略让XSS攻击的成功率大幅降低。
CSP的工作原理和核心指令
CSP通过HTTP响应头Content-Security-Policy来下发策略,浏览器收到后会解析其中的指令并严格执行。常用的指令包括以下几类:
default-src:默认策略,作为其他指令的兜底规则。如果某类资源没有单独指定,就按这个来。
script-src:控制JavaScript脚本的加载来源,这是防御XSS最关键的指令。
style-src:控制CSS样式表的加载来源。
img-src:控制图片的加载来源。
connect-src:控制XMLHttpRequest、WebSocket、fetch等连接目标。
font-src:控制字体文件的加载来源。
object-src:控制<object>、<embed>等插件的加载来源。
media-src:控制<audio>、<video>等媒体资源的加载来源。
frame-src:控制<frame>、<iframe>的加载来源。
每个指令的值可以是具体的域名、'self'(同源)、'none'(禁止)、'unsafe-inline'(允许内联脚本,不推荐)、'unsafe-eval'(允许eval,不推荐)、'nonce-xxx'(配合随机数使用)或'sha256-xxx'(配合脚本哈希值使用)。
CSP部署的具体步骤和配置方法
第一步,梳理网站的资源加载情况。在部署CSP之前,你必须搞清楚自己的页面到底加载了哪些脚本、样式、图片、字体、API接口。可以先用CSP的Report-Only模式收集信息,不会真正拦截任何东西,只是把违规情况上报给你。
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; report-uri /csp-report
第二步,根据收集到的数据编写正式的CSP策略。假设你的网站只使用自身域名加载资源,并且有一个第三方统计脚本需要引入,配置如下:
Content-Security-Policy: default-src 'self'; script-src 'self' https://analytics.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self' https://api.example.com; frame-ancestors 'none';
第三步,在Web服务器层面配置响应头。以Nginx为例,在server块或location块中添加:
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://analytics.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self' https://api.example.com; frame-ancestors 'none';";
如果使用Apache,在.htaccess或虚拟主机配置中添加:
Header set Content-Security-Policy "default-src 'self'; script-src 'self' https://analytics.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self' https://api.example.com; frame-ancestors 'none';"
如果是Node.js/Express应用,可以在中间件中设置:
app.use((req, res, next) => {
res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'self' https://analytics.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self' https://api.example.com; frame-ancestors 'none';");
next();
});
第四步,先用Report-Only模式运行一段时间,观察上报的违规日志,逐步收紧策略,直到所有合法资源都被覆盖,再切换到正式的强制执行模式。
使用nonce和hash实现更精细的内联脚本控制
很多网站存在内联脚本,比如<script>alert(1)</script>这种直接写在HTML里的代码。如果CSP的script-src只写了'self',内联脚本会被拦截。这时候有两种更安全的替代方案:nonce(随机数)和hash(哈希值)。
nonce方式:服务器每次生成页面时生成一个随机字符串,放在script标签的nonce属性里,同时在CSP头里声明这个nonce值。
Content-Security-Policy: script-src 'nonce-rAnd0m123Str1ng'
<script nonce="rAnd0m123Str1ng">
// 这个内联脚本会被允许执行
console.log('safe inline script');
</script>
hash方式:对脚本内容计算SHA256哈希值,放在CSP头里。即使攻击者修改了脚本内容,哈希值不匹配也会被拦截。
Content-Security-Policy: script-src 'sha256-CihokcEcBW4bJU53a5vQx6b3k2n9v8xY7zQ2mN5pL0k='
这两种方式都比'unsafe-inline'安全得多,因为它们只允许特定的内联脚本执行,而不是放开所有内联脚本。在实际部署中,建议优先使用nonce,因为它更灵活,不需要每次脚本改动都重新计算哈希。
CSP的Report-Only模式和违规上报机制
直接上线严格的CSP策略很容易导致页面功能异常,因为你可能漏掉了某些合法资源。所以强烈建议先用Report-Only模式过渡。这个模式不会拦截任何资源,但会把违规情况以JSON格式POST到你指定的接口。
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; report-uri /api/csp-violation-report
上报的JSON数据结构大致如下:
{
"csp-report": {
"document-uri": "https://example.com/page",
"referrer": "",
"violated-directive": "script-src",
"effective-directive": "script-src",
"original-policy": "default-src 'self'; script-src 'self'",
"blocked-uri": "https://evil.com/malicious.js",
"status-code": 200,
"script-sample": "console.log('xss')"
}
}
通过分析这些上报数据,你可以精确知道哪些资源被拦截了,从而有针对性地调整白名单。一般建议Report-Only模式至少运行两周到一个月,覆盖各种访问场景后再切换正式策略。
CSP与其他XSS防御手段的配合使用
CSP虽然强大,但它不是万能的。它不能修复已经存在的XSS漏洞,只能在浏览器层面降低被利用的风险。最佳实践是把CSP作为纵深防御体系中的一层,和其他手段配合使用:
输入验证:在服务端对用户输入进行严格校验和过滤,拒绝明显的恶意内容。
输出编码:在渲染用户数据到页面时进行HTML实体编码、JavaScript编码、URL编码等,防止数据被解析为代码。
HttpOnly Cookie:给敏感Cookie设置HttpOnly标志,即使XSS发生了,JavaScript也无法读取Cookie。
SameSite Cookie:设置SameSite=Strict或Lax,防止CSRF攻击与XSS联动。
CSP:作为最后一道防线,即使前面的防线被突破,CSP也能阻止恶意脚本执行。
这几层防御叠加在一起,才能构建真正坚固的XSS防护体系。
CSP部署中常见的坑和注意事项
第一,不要一开始就用最严格的策略。很多人上来就写script-src 'self',结果发现页面全崩了,因为第三方CDN、统计代码、广告脚本全被拦了。一定要循序渐进,先宽松再收紧。
第二,注意frame-ancestors指令。如果你的网站不需要被嵌入到别人的iframe里,一定要设置frame-ancestors 'none',这可以防止点击劫持攻击。
第三,base-uri指令容易被忽略。如果你的页面使用了<base>标签,攻击者可以通过修改base标签的href来劫持所有相对路径的资源加载。设置base-uri 'self'可以防御这类攻击。
第四,CSP只对HTML页面有效。对于用户上传的文件(比如用户上传的HTML文件作为附件下载),浏览器可能不会以你的CSP策略来渲染,攻击者可以把恶意HTML作为附件上传,诱导用户下载后在本地打开执行。这种情况需要在文件下载时设置Content-Disposition: attachment,并且确保文件存储在独立域名上。
第五,多个CSP头的合并规则。如果响应中出现多个Content-Security-Policy头,浏览器会取最严格的策略(交集)。如果同时出现Content-Security-Policy和Content-Security-Policy-Report-Only,它们互不影响,分别执行。
CSP在不同框架和平台上的实践建议
对于使用前端框架(如React、Vue、Angular)的项目,框架本身通常会处理大部分XSS防护,但CSP仍然建议部署,因为框架不能覆盖所有场景,比如第三方插件、富文本编辑器、用户生成内容等。
对于使用CDN的网站,需要把CDN域名加入对应的指令中,比如script-src 'self' https://cdn.example.com,style-src 'self' https://cdn.example.com等。
对于使用WebSocket的实时应用,需要在connect-src中加入wss://协议的域名,否则WebSocket连接会被拦截。
对于API服务(不返回HTML页面),CSP的意义不大,但如果API返回的是HTML片段(比如某些SSR场景),仍然需要配置。
总结
CSP是当前浏览器原生支持的、对抗XSS攻击最有效的机制之一。它的部署门槛不高,核心就是配置一条HTTP响应头,但要配好需要对网站资源有全面了解。建议从Report-Only模式开始,收集数据、逐步收紧,最终上线正式策略。同时不要把CSP当作唯一的防御手段,要和输入验证、输出编码、HttpOnly Cookie等措施形成纵深防御。在实际操作中,nonce和hash是处理内联脚本的最佳方案,远比'unsafe-inline'安全。只要方法得当、配置合理,CSP能让你的网站在面对XSS攻击时多一道坚固的防线。
