CSP(Content Security Policy,内容安全策略)是浏览器提供的一层安全机制,它通过HTTP响应头声明一组规则,明确告诉浏览器当前页面允许从哪些来源加载脚本、样式、图片等资源,以及禁止哪些危险行为。对于XSS防御来说,CSP的价值在于:即使攻击者成功把恶意代码注入到页面中,只要这些代码不符合策略规则,浏览器就会拒绝执行。这相当于在传统的输入过滤、输出转义之外,增加了一道基于执行环境的兜底防线。本文将从核心指令、部署方式、进阶配置和常见陷阱几个方面,系统讲解CSP的配置方法。

一、CSP的工作原理与核心指令
CSP本质上是一个白名单体系。服务器在响应头中下发策略,浏览器解析后对所有资源加载请求进行校验,凡是不在白名单内的来源一律拦截。理解这一点很重要:CSP不是黑名单过滤,默认立场是全部禁止,你需要显式地放行需要的资源。
最常用的指令是script-src,它控制JavaScript的加载来源。除此之外还有style-src(样式)、img-src(图片)、font-src(字体)、connect-src(XHR、WebSocket等连接目标)、frame-src(iframe来源)等。如果不想逐项配置,可以用default-src设置默认策略,未被具体指令覆盖的资源类型都会继承它。下面是一个基础配置示例:
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example-cdn.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self'; frame-ancestors 'none'
这个配置的含义是:默认只允许同源资源;脚本额外允许指定的CDN域名;样式允许内联;图片允许data URI和任意https来源;connect-src限制接口请求只能发往同源;frame-ancestors 'none'则禁止任何页面把本站嵌入iframe,起到防点击劫持的作用。
需要特别注意几个特殊关键字:'self'代表当前域名同源;'none'代表完全禁止;'unsafe-inline'允许内联代码(如直接写在HTML里的<script>标签);'unsafe-eval'允许eval等动态执行方式。后两个带unsafe前缀的关键字会显著削弱CSP对XSS的防护能力,因为攻击者注入的内联脚本将直接被放行。除非有明确的兼容性需求,应尽量避免使用它们,优先采用nonce或hash方案替代。
二、部署方式:响应头与meta标签的选择
CSP有两种下发方式。第一种是HTTP响应头,也就是上面示例中的形式,在Nginx、Apache或应用层代码中添加Content-Security-Policy头。这是推荐的方式,因为它支持所有指令,包括frame-ancestors和report-uri这类只能在头部生效的指令。以Nginx为例,配置写法如下:
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'nonce-{random}'; object-src 'none'; frame-ancestors 'none'" always;第二种方式是在HTML的head区域插入meta标签:
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
meta方式配置简单,不需要改动服务器,适合静态页面或无法控制响应头的环境。但它有明显的局限:策略生效时机晚于响应头,页面解析到meta标签之前加载的资源不受约束;不支持frame-ancestors、report-uri等指令。因此在正式项目中,响应头方式是更稳妥的选择,meta标签一般只作为临时过渡手段。
另外还有两个相关的响应头值得了解。Content-Security-Policy-Report-Only表示只报告不拦截,用于上线前观察策略影响;旧版的X-Content-Security-Policy和X-WebKit-CSP是历史遗留产物,现代浏览器已不再需要。
三、用nonce替代unsafe-inline
很多老项目因为大量使用内联脚本,被迫配置script-src 'unsafe-inline',这等于给XSS开了后门。更安全的做法是使用nonce机制:服务器为每次响应生成一个随机字符串,写入CSP头和对应的script标签,浏览器只执行nonce匹配的内联脚本。攻击者无法预知这个随机值,注入的脚本自然无法执行。
<!-- 响应头: script-src 'self' 'nonce-4AEemGb0xJptoIGFP3Nd' -->
<script nonce="4AEemGb0xJptoIGFP3Nd">
console.log('只有携带正确nonce的内联脚本才会执行');
</script>
<script>
console.log('这段脚本会被浏览器拦截');
</script>使用nonce时必须保证每次请求的随机值都不同,且使用加密安全的随机数生成器,否则攻击者可以预测或复用。对于单页应用中动态插入脚本的情况,也可以用hash方式:对内联脚本内容计算SHA256哈希写入策略,script-src 'sha256-基64编码的哈希值'。hash适合内容固定的脚本,内容稍有变化就要重新计算,维护成本比nonce高。
需要注意的是,一旦script-src中出现了nonce或hash,浏览器会忽略同一指令中的'unsafe-inline',这是CSP Level 2规定的优先级规则,可以防止配置冲突导致防护失效。
四、Report-Only模式与上线策略
CSP配置过严会直接导致页面白屏、样式错乱,这在生产环境是不可接受的。正确的上线路径是先用Content-Security-Policy-Report-Only观察一段时间,再切换到强制模式。
add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self'; report-uri https://log.ipipp.com/csp-report" always;
配置了report-uri后,每当有资源被策略拒绝,浏览器会向指定地址发送一份JSON格式的违规报告,包含被拦截的URL、违反的指令、页面地址等信息。在新版规范中也可以用report-to配合Report-To响应头实现同样的功能。收集到的报告可以帮你发现遗漏的资源域名,逐步完善白名单,确认没有误伤后再把头部切换为Content-Security-Policy正式启用。
在实际收严策略的过程中,建议按资源类型分步推进:先收紧object-src和base-uri(这两项设为'none'和'self'几乎没有副作用),再处理script-src的nonce改造,最后清理样式和图片的放行范围。每一步都通过报告验证,比一次性上线完整策略要稳妥得多。
五、常见配置陷阱与生产环境示例
第一个常见错误是白名单里写了带通配符的域名,比如https://*.com或https://*,这等于放行了整个互联网,CSP形同虚设。第二个是误以为配置了CSP就不需要输出转义,CSP只是纵深防御的一环,不能替代正确的编码习惯。第三个是忽略了base-uri,攻击者若能注入<base>标签改变页面基准URL,可以劫持相对路径的脚本加载,因此建议显式设置base-uri 'self'。
下面是一份适合生产环境的参考配置,覆盖了常见的资源类型,并加上了升级HTTP请求的指令:
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{random}'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self' https://api.ipipp.com; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'; upgrade-insecure-requests其中form-action限制表单提交目标,防止注入的表单把用户数据发到外部;upgrade-insecure-requests让浏览器自动把页面中的HTTP请求升级为HTTPS。整体思路是默认全部收紧,再按实际需求逐项放开。CSP的配置不是一次性的工作,随着业务迭代要持续维护白名单,配合报告机制定期审查,才能让这道防线真正发挥作用。