网站内容安全策略:CSP的配置、绕过风险与部署
一、CSP到底挡住什么
CSP通过HTTP响应头下发规则,浏览器据此决定哪些资源能加载。它主要防两类问题:XSS注入的恶意脚本、未经授权的第三方域被嵌进页面。
它不是防火墙。CSP不能挡SQL注入,也不能挡服务器被入侵,它管的是浏览器侧的资源加载。和HTTPS、HSTS是配合关系,HTTPS配置方法见独立站HTTPS配置。
独立站常见的痛点是第三方脚本太多:GA4、GTM、Hotjar、在线客服、广告像素。每加一个脚本,CSP白名单就要加一条域名,否则脚本被浏览器拦掉。第三方脚本治理见独立站第三方脚本治理。
一个常被忽略的事实是,CSP对SEO几乎没有负面影响。Googlebot本身不执行JS之外的资源限制,配好CSP不会影响收录。担心CSP影响抓取是多余的。
| 指令 | 管什么 | 建议值 |
|---|---|---|
| default-src | 所有资源兜底 | ‘self’ |
| script-src | JS脚本来源 | ‘self’ + 统计域名 |
| style-src | CSS来源 | ‘self’ ‘unsafe-inline’ |
| img-src | 图片来源 | ‘self’ data: + CDN |
| object-src | 插件 | ‘none’(必关) |
| frame-ancestors | 谁能嵌套本站 | ‘none’ |
二、三步部署:报告、收敛、强制
最常见的错误是直接上Content-Security-Policy头。这样会把站内所有未列入白名单的脚本立刻拦掉,GA4、在线客服、像素全挂,业务方第一时间投诉。
正确做法是先用Content-Security-Policy-Report-Only头。浏览器只报告违规、不拦截,你收集两周日志,把所有真实用到的域名加进白名单。
场景示例一:给WordPress站加CSP,先发Report-Only。
两天后报告里出现googletagmanager.com、hotjar.com、youtube.com三个域名。
逐条加进script-src和frame-src。两周后报告每天只剩个位数,再切到强制模式。
| 阶段 | 响应头 | 行为 | 持续时间 |
|---|---|---|---|
| 第一步 | Content-Security-Policy-Report-Only | 只报告不拦截 | 14天 |
| 第二步 | 同上,逐步补白名单 | 违规逐日下降 | 直到个位数 |
| 第三步 | Content-Security-Policy | 真正拦截 | 长期 |
| 全程 | 加report-to或report-uri | 收集违规上报 | 一直开着 |
内联脚本是最大的坑。WordPress主题和很多插件直接在页面里写<script>,CSP默认拦内联。两个办法:一是把内联脚本抽到外部js文件,二是用sha256哈希放行具体那一段,而不是图省事开unsafe-inline。
style-src比script-src松一些。古腾堡编辑器和很多主题会输出内联style,style-src临时留着unsafe-inline问题不大。
但script-src绝对不能开unsafe-inline,这是CSP防XSS的核心防线。两条指令的松紧标准要区别对待。
三、绕过风险:别让CSP形同虚设
CSP配错等于没配。以下三种写法会让这道墙直接失效。
第一种是开unsafe-inline。它允许任何内联脚本执行,XSS注入的恶意脚本也能跑,CSP的核心防线就破了。第二种是开通配符*,等于不限制来源。第三种是漏了object-src,老浏览器还能通过Flash或插件绕过。
场景示例二:图省事在script-src里写了’unsafe-inline’。
结果两个月后站点被注入一段内联钓鱼脚本,CSP完全没拦住。
正确做法是用nonce,每个请求生成一次性随机数,只放行带这个数的内联脚本。
| 错误写法 | 风险 | 替代方案 |
|---|---|---|
| script-src * | 任意第三方脚本可加载 | 逐个列白名单域名 |
| script-src ‘unsafe-inline’ | XSS注入脚本照常执行 | 抽外部文件或用nonce/哈希 |
| 不写object-src | 老插件可被滥用 | object-src ‘none’ |
| 不配frame-ancestors | 页面被恶意iframe嵌套 | frame-ancestors ‘none’ |
CSP和合规是两回事。GDPR的Cookie同意是另一套机制,见独立站GDPR合规。CSP防的是脚本注入,不解决Cookie同意问题。服务器层面的登录和文件权限见独立站安全加固。
nonce和哈希怎么选?固定的内联脚本用哈希,动态生成的内联脚本用nonce。WordPress主题里固定的追踪代码段算哈希放行;每次请求内容不同的脚本块用nonce。
部署位置也有讲究。CSP响应头最好在Nginx或CDN层下发,不要靠PHP应用输出。应用层输出会在PHP故障时丢失,CDN层下发则稳定。
| 部署位置 | 稳定性 | 适用场景 |
|---|---|---|
| Nginx配置 | 高,应用挂了也在 | 自建服务器 |
| CDN规则 | 高,全球生效 | 用了Cloudflare等 |
| PHP应用输出 | 低,故障时丢失 | 临时测试 |
报告接收端点也要专门建。report-uri指向的地址不能被CSP自己拦,否则报告发不出来。
独立站可以用现成的报告收集服务,或者在Nginx里配一个专门的log endpoint接收上报,写进单独的日志文件,不进主站流量日志。
报告量大了之后要聚合。每天可能收到几百条上报,逐条看不过来,按违反的指令和被拦域名分组统计,重点关注每天新增的域名。
有一个常见误区:以为CSP上了就万事大吉。实际上每次加新的第三方脚本、每次主题升级,都可能引入新的违规源。
所以切到Enforce之后,report-uri不能关。它是你发现”新加的脚本忘了加白名单”的唯一信号来源。
浏览器支持也要提一句。现代浏览器(Chrome、Edge、Firefox、Safari近两年版本)都完整支持CSP 3。老IE可以直接忽略,B2B独立站的访客基本不会用IE。
还有一个部署细节:CSP头要同时给http和https都下发。只在https上下发,http回源阶段的请求就不受CSP保护。
切到Enforce之后,建议保留一条Report-Only头并行存在一段时间,既拦截又上报,万一漏了什么还能在报告里看到。
CSP不是一次性项目。每加一个新的第三方脚本(客服、聊天、视频、广告),都要先在报告模式观察到它出现在违规里,再把对应域名加进白名单,然后才切到生产。
把CSP检查加进新脚本上线清单:加脚本的同时加白名单,不要等上线后被用户报”控件不显示”才想起CSP。
一个判断标准:如果某段第三方脚本无法被改造成外部文件、也无法计算哈希,那就要重新评估这个第三方是否值得用。依赖unsafe-inline的第三方服务本身就是技术债。
部署完成后,用浏览器控制台的Security面板逐页抽查。首页、产品页、博客页、表单页各看一遍,确认没有红色违规提示。
抽查不通过的页面,回到报告里找对应域名,补进白名单再观察一天。这个循环重复两三轮就稳了。
CSP不是一次部署完就结束的安全项,它和你的第三方脚本库一起长期演进。每加一个新脚本,都要同步更新一次白名单,千万别漏配任何一个新出现的第三方脚本域名。
| 报告来源 | 含义 | 处置 |
|---|---|---|
| script-src违规 | 新脚本没加白名单 | 加域名或抽外部文件 |
| img-src违规 | 图片外链来自新域 | 加CDN或改相对路径 |
| frame-src违规 | 嵌入了新iframe | 加视频域名或去掉 |
| base-uri违规 | 页面被注入base标签 | 立即排查XSS |
FAQ 常见问题
CSP应该一上线就用强制模式吗?
WordPress主题里的内联脚本被CSP拦了怎么办?
CSP和HTTPS、HSTS是什么关系?
为什么我的CSP报告里全是违规?
CSP能挡住SQL注入吗?
report-uri还需要保留吗?
写在最后
CSP的难点不在写规则,在于耐心跑报告模式。两周的观察期换来后续几年的脚本可控,这笔账划算。
落地三步:先发Report-Only跑14天;按报告把真实域名逐个收进白名单,用哈希替代unsafe-inline;违规降到个位数后切强制,同时保留report持续监控。
一句可以记住的话:CSP的安全水位取决于白名单有多干净——留着unsafe-inline和通配符,这道墙等于没砌。
