CSP 内容安全策略:减少 XSS 风险
什么是内容安全策略 (CSP)
内容安全策略(Content Security Policy,简称 CSP)是一个额外的安全层,用于帮助检测和缓解某些类型的攻击,包括跨站脚本(XSS)和数据注入攻击。它通过让开发者精确控制浏览器允许加载的资源来源来实现这一目标。
在没有 CSP 的情况下,浏览器会信任页面请求的所有资源。如果攻击者能在页面中注入恶意脚本,该脚本就会执行。CSP 从根本上改变了这一信任模型:它要求开发者为浏览器提供一个“白名单”,明确哪些源是可信任的。浏览器仅会执行或渲染来自这些白名单源的资源,而拒绝一切未明确允许的内容。
可以把 CSP 想象为一栋大楼的门禁系统。你提前告知安保人员:只有持有特定公司工卡(脚本来源)、在前台登记(样式来源)或者由指定供应商配送(图片来源)的人才能进入。任何不明身份者都会被拦下。这样,即使大楼内部出现了一个试图混入的陌生人(恶意脚本),门禁系统依然会阻止其执行。
为什么需要 CSP:直面 XSS 威胁
跨站脚本(XSS)是 Web 应用中最常见且危险的安全漏洞之一。攻击者利用输入过滤不严的漏洞,将恶意脚本注入到正常网页中。一旦其他用户访问该页面,脚本便在其浏览器中运行,窃取 Cookie、会话令牌,甚至重写页面内容。
传统的 XSS 防御依赖严格的输入输出编码,但在复杂应用中,人为疏忽或框架漏洞仍可能导致注入点出现。CSP 提供了纵深防御:即使攻击者成功注入了脚本标签,只要该脚本不在白名单中,浏览器就会拒绝执行。
例如,一个反射型 XSS 攻击可能将 <script>alert('XSS')</script> 插入页面。在没有 CSP 的情况下,浏览器会弹出提示框。而配置了严格 CSP 的页面,其策略不允许任何内联脚本,这个注入的脚本将不会被运行,浏览器控制台会输出一条策略冲突的警告。
CSP 如何工作:从 HTTP 头部到执行引擎
CSP 可以通过两种方式启用:
Content-Security-PolicyHTTP 响应头<meta>标签
生产环境中强烈建议使用 HTTP 头部,因为它可以覆盖所有内容类型,并且某些指令(如 frame-ancestors)只能通过头部定义。
一个典型的 CSP 头部看起来像这样:
Content-Security-Policy: default-src 'self'; script-src 'self' https://apis.example.com; style-src 'self' 'unsafe-inline'; img-src *;
当浏览器接收到带有此头部的响应后,会解析策略中的各个指令。在加载页面过程中,对于每一个要请求的子资源(脚本、样式、图片、字体等)或将要执行的操作(如内联脚本执行、表单提交),浏览器都会检查当前获取的内容是否匹配指令中的某个来源。如果不匹配,浏览器会阻断该资源加载或执行,并在开发者工具中报告违规。
采用 <meta> 标签(有限场景)
某些仅需局部控制且无法修改服务器配置的场景(如静态网站部署、部分 CMS 主题)中,可以在 HTML 的 <head> 内使用:
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src https://cdn.example.com;">
需要注意,<meta> 方式不支持 frame-ancestors、report-uri(建议使用 report-to)等指令,且不能用于报告模式。
核心指令详解
CSP 的强大之处在于其细粒度的指令。以下是最常用的资源指令:
default-src
作为回退机制,当某个具体指令(如 script-src、style-src)未设置时,浏览器会使用 default-src 的值。通常建议设置 default-src 'self',然后为脚本等高风险资源单独收紧或放宽。
script-src
控制允许执行的 JavaScript 来源,是防御 XSS 最关键的指令。
可能的值包括:
'self':仅允许同源脚本文件。'unsafe-inline':允许内联脚本(如<script>...</script>、onclick属性)。启用此项会大幅削弱 XSS 防御。'unsafe-eval':允许eval()、setTimeout(string)等动态代码执行。'nonce-{随机字符串}':允许携带指定 nonce 属性的内联脚本,实现安全的内联脚本白名单。'sha256-{hash}':允许与指定哈希值匹配的内联脚本。- 域名或协议:如
https://apis.google.com。
最佳实践是避免 'unsafe-inline' 和 'unsafe-eval',使用 nonce 或 hash 机制来安全使用内联脚本,并将第三方脚本源明确列出。
style-src
控制 CSS 样式来源,同样支持 'self'、域名、nonce、hash 以及危险的 'unsafe-inline'。
img-src
定义允许加载图片的源。常用 'self' data: https: 来允许同源图片、内嵌 base64 图片和 HTTPS 图片。
font-src 和 frame-src
font-src 管理字体加载源,frame-src 则控制页面可以嵌入哪些来源的 <iframe>。frame-src 已被 frame-src 取代,但为了兼容性可同时设置二者。
connect-src
限制通过脚本接口(如 fetch、XMLHttpRequest、WebSocket)发起的连接目标,适用于 API 端点白名单。
media-src、object-src 和 base-uri
object-src控制<object>、<embed>、<applet>的加载,建议设为'none',防止插件注入。base-uri限制<base>元素的来源,防止攻击者通过修改基 URL 劫持相对路径资源。form-action限定表单提交的目标地址,可防范钓鱼或数据外泄。
从零开始构建一个安全的 CSP 策略
第一步:确定应用加载了哪些资源
使用 Chrome DevTools 的 Network 面板或 CSP 评估工具(如 CSP Evaluator)检查页面实际用到的所有域名:脚本来自哪里?样式?图片?API 调用?字体?
第二步:从严格策略起步
先制定一个约束性强的策略,并逐步放宽:
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';
该策略表示:所有资源仅允许从同源加载;禁止页面被嵌入框架;限制 <base> 和表单提交到本域。
第三步:使用非侵入模式收集违规报告
在添加 Content-Security-Policy 强制实施之前,先使用 Content-Security-Policy-Report-Only 头部。这会让浏览器仅报告违规而不阻断资源。配合 report-uri 或 report-to 指令,收集所有冲突,然后据此调整白名单。
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report-endpoint
服务端接收 JSON 格式的违规报告,文件中会包含 blocked-uri、violated-directive、document-uri 等字段,帮助开发者定位问题。
第四步:逐步扩展白名单,同时消除 unsafe-inline
对于必须的内联脚本,不要添加 'unsafe-inline'。修改后端模板,为每个合法内联脚本生成一个唯一的 nonce:
<script nonce="随机且不可预测的字符串">
// 安全的内联脚本
</script>
然后在策略中添加 script-src 'nonce-随机字符串'。该 nonce 必须在每次响应中重新生成,且不可预测。对于少量静态内联脚本,也可使用 hash 方式:计算脚本内容的 SHA-256 哈希,并在策略中写为 'sha256-哈希值'。
第五步:处理第三方资源
很多网站会用到 Google Analytics、CDN、社交媒体挂件等第三方服务。将它们严格添加到相应指令中:
script-src 'self' https://www.googletagmanager.com https://www.google-analytics.com;
img-src 'self' data: https://trusted-cdn.com;
对于需要允许用户生成内容的站点,要格外小心,尽量确保用户内容被隔离(例如使用沙盒 iframe),而不是直接信任。
高级安全指令:防范更多攻击
frame-ancestors 防点击劫持
代替老旧的 X-Frame-Options,CSP 的 frame-ancestors 可精确指定谁可以嵌入当前页面:
frame-ancestors 'self' https://trusted-partner.com
设为 'none' 则完全禁止被嵌入 frame。
upgrade-insecure-requests
当网站正在从 HTTP 迁移到 HTTPS 时,此指令可以让浏览器自动将 HTTP 资源请求升级为 HTTPS,避免混合内容问题:
upgrade-insecure-requests;
require-trusted-types-for 防 DOM XSS
Trusted Types 是 CSP 的扩展,通过在策略中添加 require-trusted-types-for 'script',强制浏览器只接受经过安全策略函数创建的可信类型值(如 innerHTML 赋值),从根源上消除 DOM XSS。这是一个更现代的防御,建议在新项目中研究启用。
常见陷阱与测试策略
- 过度依赖通配符
*:允许所有来源会令 CSP 形同虚设。如script-src *允许任意域名的脚本执行,无法防御恶意脚本注入。 - 遗漏指令:设置了
script-src但忘记style-src,样式仍可能被利用进行数据窃取(虽然风险较低)。 - nonce 可预测或重用:nonce 必须每次生成随机且不同,否则攻击者一旦猜到 nonce,即可绕过限制。
- 在报告模式中自我安慰:长期只使用
Report-Only而不启用强制执行,无法实际保护用户。 - 使用
unsafe-eval且不设防:若必须使用eval,请确保所有输入已严格过滤。尽可能寻找替代方案。
测试 CSP:使用浏览器的控制台查看 CSP 违规。在 Chrome DevTools 的 “Security” 面板中也能看到 CSP 状态。自动化测试中,将 CSP 违规视为测试失败,确保不会引入新的错误。
结语:CSP 不是银弹,却是关键防线
CSP 并不能保证应用绝对不会被 XSS 攻击,但它将漏洞利用的难度提升了数个量级。一个精心设计的 CSP 策略可以阻止大多数反射型和存储型 XSS 攻击,以及某些类型的点击劫持和数据注入。将其与输入验证、输出编码、安全 Cookie 标记(HttpOnly、Secure、SameSite)结合使用,形成深度防御体系,才能为你的 Web 应用构建足够坚固的安全壁垒。
着手实施时,请记住:从报告模式开始,收集数据,逐步收紧策略,并定期审计你的白名单。内容安全策略是一项持续演进的安全配置,随着应用变化而调整,它将在无声中守护每一个访问者的安全。