Spring Security 7.2.0-RC1 给 CSP 加了 nonce:策略里写 script-src 'nonce-{nonce}',响应头里的 {nonce} 会被换成当次请求的随机值,同一请求里模板用请求属性 _csp.nonce 填到 <script nonce="...">。Chrome headless 里,nonce 对上时页面状态变成 ALLOWED;去掉 nonce 或填错值时状态停在 PENDING,控制台报 CSP 拒绝执行。

环境是 Spring Boot 4.2.0-RC1(BOM 管理的 spring-security.version 就是 7.2.0-RC1)、Spring Framework 7.1.0-RC1、JDK 21.0.12.1。发行页 7.2.0-RC1 把 “Support nonce-based Content Security Policy” 列在 New Features;实现见 gh-18499。同页还有 WebAuthn Registration Page 的 no-script 改进,这里不展开。ContentSecurityPolicyNonceGeneratingFilter 和 ContentSecurityPolicyNonce 都标着 @since 7.2;7.1.2 / 7.1.1 / 7.0.0 的 spring-security-web jar 里没有这些类。

配置

Headers DSL 的 policyDirectives 支持占位符 {nonce}。配了 CSP 后,HeadersConfigurer 会在 HeaderWriterFilter 前面挂上 ContentSecurityPolicyNonceGeneratingFilter(源码里是一配 CSP 就加,不依赖占位符是否出现)。默认请求属性名是 _csp,可用 nonceAttributeName 改掉。

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
	http.authorizeHttpRequests((auth) -> auth.anyRequest().permitAll())
		.csrf((csrf) -> csrf.disable())
		.headers((headers) -> headers.contentSecurityPolicy((csp) -> csp
			.policyDirectives("default-src 'self'; script-src 'nonce-{nonce}'; style-src 'self'")
			.nonceAttributeName("_csp")));
	return http.build();
}

页面用 Thymeleaf 把同一个 nonce 写进脚本标签:

<p id="status">PENDING</p>
<script th:attr="nonce=${_csp.nonce}">
document.getElementById('status').textContent = 'ALLOWED';
</script>

响应头和 HTML

对 /with-nonce 抓一次响应,头里的 nonce 和 HTML 里的 nonce 属性是同一个值 lSQfnUd6OlYPC4RttCLT+w:

Content-Security-Policy: default-src 'self'; script-src 'nonce-lSQfnUd6OlYPC4RttCLT+w'; style-src 'self'

<p id="status">PENDING</p>
<script nonce="lSQfnUd6OlYPC4RttCLT+w">
document.getElementById('status').textContent = 'ALLOWED';
</script>

/without-nonce 同样有 CSP 头,脚本标签没有 nonce:

<p id="status">PENDING</p>
<script>
document.getElementById('status').textContent = 'SHOULD_NOT_RUN';
</script>

/wrong-nonce 头里是 nonce-VYW71Z1jFV3YB48KuYzvSg,脚本上却是 not-the-real-nonce:

Content-Security-Policy: default-src 'self'; script-src 'nonce-VYW71Z1jFV3YB48KuYzvSg'; style-src 'self'

<p id="status">PENDING</p>
<script nonce="not-the-real-nonce">
document.getElementById('status').textContent = 'SHOULD_NOT_RUN';
</script>

连续两次请求,nonce 不同:

req1: Content-Security-Policy: default-src 'self'; script-src 'nonce-8svsbnBpgJYieijkgzpHjA'; style-src 'self'
req2: Content-Security-Policy: default-src 'self'; script-src 'nonce-gMNcNzCP6bBvOTAELkVEdw'; style-src 'self'
DIFFERENT_NONCE

Chrome 里谁跑了

用 Google Chrome 155 headless --dump-dom 打开三个地址。带对 nonce 的页面把 #status 改成了 ALLOWED;没有 nonce 和填错 nonce 的页面仍是 PENDING。

$ google-chrome --headless=new --disable-gpu --no-sandbox --virtual-time-budget=3000 --dump-dom http://127.0.0.1:18081/with-nonce
<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="UTF-8"><title>with-nonce</title></head>
<body>
<p id="status">ALLOWED</p>
<script nonce="">
document.getElementById('status').textContent = 'ALLOWED';
</script>


</body></html>
$ google-chrome --headless=new --disable-gpu --no-sandbox --virtual-time-budget=3000 --dump-dom http://127.0.0.1:18081/without-nonce
<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="UTF-8"><title>without-nonce</title></head>
<body>
<p id="status">PENDING</p>
<script>
document.getElementById('status').textContent = 'SHOULD_NOT_RUN';
</script>


</body></html>
$ google-chrome --headless=new --disable-gpu --no-sandbox --virtual-time-budget=3000 --dump-dom http://127.0.0.1:18081/wrong-nonce
<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="UTF-8"><title>wrong-nonce</title></head>
<body>
<p id="status">PENDING</p>
<script nonce="">
document.getElementById('status').textContent = 'SHOULD_NOT_RUN';
</script>


</body></html>

同一轮用 CDP 读到的控制台(PASS2)里,后两种明确被 CSP 拦住;带对 nonce 的那次状态是 ALLOWED:

/without-nonce status=PENDING
/without-nonce MSG: Executing inline script violates the following Content Security Policy directive 'script-src 'nonce-C055SQa4QxOgDfUuUqsWbA''. Either the 'unsafe-inline' keyword, a hash ('sha256-gy5MncI9khe47GtDHMVjkwIMqPfvSLIFP9ajgm3gHRo='), or a nonce ('nonce-...') is required to enable inline execution. The action has been blocked.
/wrong-nonce status=PENDING
/wrong-nonce MSG: Executing inline script violates the following Content Security Policy directive 'script-src 'nonce-Evxg/6rP52eJxvozxW/S9g''. Either the 'unsafe-inline' keyword, a hash ('sha256-gy5MncI9khe47GtDHMVjkwIMqPfvSLIFP9ajgm3gHRo='), or a nonce ('nonce-...') is required to enable inline execution. The action has been blocked.
/with-nonce status=ALLOWED

版本边界

CSP 头本身从 Security 4.1 就有(ContentSecurityPolicyHeaderWriter 标 @since 4.1)。{nonce} 替换、ContentSecurityPolicyNonceGeneratingFilter、nonceAttributeName / requestMatcher 是 7.2 新加的,发行说明和 @since 7.2 一致。Boot 4.1.2 管理的是 Security 7.1.2;这次用 Boot 4.2.0-RC1 是因为它的 BOM 已经指向 7.2.0-RC1,不必再手写 Security 版本。

— 感谢阅读 —
广告

一起交流

分享你的思考,让讨论更进一步。