CSP 如何防止 XSS?nonce、hash 和 strict-dynamic 怎么选?
直接回答
Content Security Policy,简称 CSP,是浏览器执行的一套资源加载和脚本执行规则。它不能替代输出编码,也不能把脏 HTML 自动洗干净,但它能在 XSS payload 混进页面后继续拦一刀:不允许内联脚本执行,不允许加载未知域名脚本,不允许 eval,也能限制表单提交、iframe 嵌套和基础 URL。对安全来说,CSP 更像安全带,不是刹车;撞车前最好别撞,真撞上时它能降低伤害。
一个可落地的 CSP 通常从 Report-Only 开始,先收集违规报告,再逐步收紧。新项目优先使用 nonce 或 hash,少用 'unsafe-inline';老项目如果内联脚本很多,可以先把脚本拆出去,再引入 nonce。CSP 的难点不是写出一条很漂亮的 header,而是在业务脚本、监控、A/B 测试、支付、客服和广告之间找到可维护的边界。
推荐配置
httpContent-Security-Policy: default-src 'self'; script-src 'nonce-r4nd0m' 'strict-dynamic' https:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; img-src 'self' https: data:; connect-src 'self' https://api.example.com; report-uri https://report.example.com/csp;
nonce 必须每个响应重新生成,并且只发给服务器确认过的脚本标签。不要把 nonce 写死在模板、缓存片段或全局变量里,否则攻击者只要读到一次就能复用。object-src 'none'、base-uri 'self'、frame-ancestors 这些指令常被忽略,但它们能挡住不少老式插件、base 标签劫持和点击劫持组合拳。
追问
CSP 能不能彻底解决 XSS?
不能,CSP 是缓解措施,不是根治手段。真正的根治仍然是按上下文输出编码、富文本白名单清洗、避免危险 DOM API。CSP 的价值在于漏洞发生后减少脚本执行和数据外传,例如阻止未知域名脚本、限制 connect-src。边界是浏览器兼容、策略误配、已有可信脚本被利用时,CSP 仍可能被绕过。
nonce、hash 和白名单域名该怎么选?
nonce 适合服务端渲染页面,因为每次响应都能给合法脚本发一个随机通行证。hash 适合内容固定的内联脚本,脚本一改 hash 就要更新,维护成本更高但不依赖运行时注入。域名白名单看起来简单,却容易因为信任整个 CDN 或第三方域名而扩大攻击面。取舍上,新系统优先 nonce/hash,域名白名单只给确实需要的外部资源,别把 https: 当万能许可。
为什么很多 CSP 最后都加了 'unsafe-inline'?
通常是历史包袱:页面里有大量内联事件、内联脚本、老组件或第三方片段,一关就坏。加 'unsafe-inline' 能快速恢复业务,但也会让 CSP 对内联 XSS 的防护大打折扣。更好的迁移方式是先用 Content-Security-Policy-Report-Only 收集问题,再按页面分批移除内联脚本。踩坑点是报告量会很大,需要聚合去重,不然安全团队很快被噪音淹没。
strict-dynamic 有什么用?
strict-dynamic 表示只要一个带 nonce 或 hash 的可信脚本被允许执行,它动态加载的后续脚本也可以被信任。它适合现代前端打包和运行时加载场景,能减少维护一长串脚本域名的痛苦。边界在于你必须先保护好入口脚本,入口脚本如果被污染,动态信任会放大问题。老浏览器支持也要评估,必要时保留兼容性的域名策略。
CSP 上线最容易踩哪些坑?
第一是把策略一次性收太紧,导致支付、验证码、地图、监控全挂。第二是只配置 script-src,却忘了 connect-src、frame-src、form-action,攻击者仍可能把数据送出去。第三是没有监控报告,线上误杀只能靠用户投诉。实战里建议按页面或业务域灰度,先 Report-Only,再 Enforcement,并把违规报告接入告警和例外审批。