什么是CSP
CSP 全称为 Content Security Policy,即内容安全策略。是一个附加的安全层,有助于缓解 XSS。配置 CSP 涉及将 Content-Security-Policy HTTP 标头添加到网页并设置值以控制允许用户代理为该页面加载哪些资源。
参考文档:https://web.dev/strict-csp/#step-1:-decide-if-you-need-a-nonce-or-hash-based-csp
CSP unsafe-inline
CSP 的默认策略是不允许 inline 脚本执行,所以当我们没有必要进行脚本 inline 时,CSP 域名白名单的机制足以防范注入脚本的问题。然而在实际项目中,我们还是会因为一些场景需要将部分脚本进行 inline。于是需要在 CSP 的规则中增加 script-src ‘unsafe-inline’ 配置,允许了 inline 资源执行。但也带来了新的安全隐患。
允许 inline 资源执行,也意味着当恶意代码通过 inline 的方式注入到页面中执行时,页面将变得不再安全。如富文本中被插入一段 script 代码(没被转义),或者是通过浏览器插件的方式进行代码注入等方式。
1 | Content-Security-Policy: script-src 'unsafe-inline' |
CSP nonce
为了避免上述问题,我们可以使用 nonce 方式加强的 CSP 策略。nonce 方式是指每次页面访问都产生一个唯一 id,通过给内联脚本增加一个 nonce 属性,并且使其属性值 (id) 与返回的 CSP nonce-{id} 对应。只有当两者一致时,对应的内联脚本才被允许执行。于是,即使网页被注入异常的脚本,因为攻击者不知道当时 nonce 的随机 id 值,所以注入的脚本不会被执行。从而让网页变得更加安全。
1 | Content-Security-Policy: script-src 'nonce-23c3452d64' |
那么,当我们通过动态生成脚本并进行插入时,nonce 也会将我们的正常代码拦截在外。所以在这种场景下,我们需要配套使用 CSP 提供的 ‘strict-dynamic’,’strict-dynamic’ 模式允许让被信任的脚本插入并放行正常脚本执行。
1 | Content-Security-Policy: script-src 'nonce-23c3452d64' 'strict-dynamic' |
Nonce 的部署方式
前端
scirpt 标签增加 nonce 属性
我们可以通过构建的方式为页面中 script 标签添加 nonce 属性,并添加一个占位符,如
1 | <script nonce="NONCE_TOKEN"> |
后端
生产唯一 id,在 CSP 返回头中添加 nonce-{id} 并将 id 替换 html 上的 nonce 占位符
方式一:服务端处理
- 当页面在服务端渲染时,html 作为模板在服务端进行处理后输出,我们可以在后端生产唯一 id
- 通过模板变量将 id 注入到 html 中实现替换 NONCE_TOKEN 占位符
- 与此同时,将 CSP 返回头进行对应设置
方式二:Nginx 处理
- Nginx 中可以使用内置变量的 $request_id 作为唯一 id,而当 nginx 版本不支持时,则可以借助 lua 去生产一个 uuid;
- 接着通过 Nginx 的 sub_filter NONCE_TOKEN ‘id’ 将页面中的 NONCE_TOKEN 占位符替换为 id,或者使用 lua 进行替换;
- 最后使用 add_header Content-Security-Policy “script-src ‘nonce-{id}’ … 添加对应的 CSP 返回头。
当然,为了避免攻击者提前注入一段脚本,并在 script 标签上同样添加了 nonce=”NONCE_TOKEN” ,后端的 “误” 替换,导致这段提前注入的脚本进行执行。我们需要保密好项目的占位符,取一个特殊的占位符,并行动起来吧!
配置实践
最近公司网站Lighthouse的“最佳实践”项评分,比较低,给出的建议是:
开始配置,nginx配置文件添加header,添加配置内容如下:
1 | add_header Content-Security-Policy "script-src 'nonce-$request_id' 'strict-dynamic';object-src 'none';base-uri 'none'"; |
重新评分,提示如下:
1 | Consider adding 'unsafe-inline' (ignored by browsers supporting nonces/hashes) to be backward compatible with older browsers. |
这个只需要在配置'strict-dynamic'
后面添加'unsafe-inline'
。
1 | add_header Content-Security-Policy "script-src 'nonce-$request_id' 'strict-dynamic' 'unsafe-inline';object-src 'none';base-uri 'none'"; |
改完继续评分,只剩一个优化项了:
'unsafe-inline'
后面加上https:
,完整配置如下:
1 | add_header Content-Security-Policy "script-src 'nonce-$request_id' 'strict-dynamic' 'unsafe-inline' https:;object-src 'none';base-uri 'none'"; |
遇到的问题
服务端已经将js文件的nonce属性和属性值设置好了,查看源代码也可以看到属性一切正常,但是查看浏览器元素发现nonce属性没有值。
源代码:1
<script src="https://www.xxx.com/static/js/jquery-3.6.0.min.js" nonce="nonce-bd2e592a2454ec2b98e24e2a382eb555"></script>
审查元素:
1
<script src="https://www.xxx.com/static/js/jquery-3.6.0.min.js" nonce></script>
问题原因:此行为是在 https://github.com/whatwg/html/pull/2373 规范的更新中添加的(隐藏
nonce
内容属性值)。也就是说,DOM 检查器不会显示该script
元素上的nonce
属性的值。更准确地说:如果文档使用Content-Security-Policy
标头提供,并且浏览器在该标头中应用策略,您将不会在script
上看到nonce
属性的值。如果您不提供带有
Content-Security-Policy
标头的文档,或者浏览器不应用其中的策略,您将看到nonce=DhcnhD3khTMePgXw
中的script
元素检查员。因此,在 DOM 检查器中缺少
nonce
属性的值实际上表明事情正在按预期工作。也就是说,它表示浏览器正在检查值是否与Content-Security-Policy
标头中的任何nonce-*
源表达式匹配。它在浏览器中的工作方式是:浏览器将
nonce
属性的值移动到一个“内部槽”以供浏览器自己使用。所以它对浏览器仍然可用,但对 DOM 隐藏。服务端正常返回了nonce属性值,js文件依旧被CSP屏蔽
原因:服务端替换占位符的时候,只需要id,不需要前面的nonce-
,,参考往上翻,参考CSP noncenginx替换占位符只有第一个替换成功,其他不变
原因:没有关闭只替换一次开关,只需要nginx配置里加上sub_filter_once off;
即可,重启nginx生效
小结
CSP 的应用场景越来越多,逐步地优化策略才能更好地守护我们的项目安全。