Support nonce-based Content Security Policy#18499
Conversation
|
It seems that #18840 may affect the two-step assumption made here. If HTTP headers are written eagerly, |
Signed-off-by: Ziqin Wang <ziqin@wangziqin.net>
When strict Content Security Policy is used, web browsers block inline
<script> or <style> blocks in HTML to mitigate XSS attacks injecting
malicious inline blocks. To allow intended inline blocks, web developers
can generate a hard-to-guess nonce and specify it in both the CSP and
allowed inline blocks.
Currently, Spring Security only supports specifying static content
security policy directives. This commit adds support for dynamically
generating a secure random nonce for CSP:
- NonceGeneratingFilter & NonceGeneratingWebFilter are added to
generate a nonce and set it as a request attribute,
- ContentSecurityPolicyHeaderWriter &
ContentSecurityPolicyServerHttpHeadersWriter are modified to read
the _csp_nonce attribute and write it to the Content-Security-Policy
header, replacing the {nonce} placeholder in the given
policyDirectives string.
The whole process is separated in two steps because by default a header
writer cannot set a request attribute visible to views for rendering the
nonce in HTML.
`_csp_nonce` is chosen as the default attribute name because it has a
similar format with the existing `_csrf` attribute. The attribute name
is configurable.
This commit implements spring-projectsgh-10826.
Signed-off-by: Ziqin Wang <ziqin@wangziqin.net>
This commit adds support for configuring nonce-based CSP with Java lambda or Kotlin DSL. By default, Spring Security adds protection headers for all served resources. This was not a problem in the past because header values were static. However, with the introduction of a dynamic nonce in the CSP, the caching property of static asserts served may change. With this in mind, this commit also adds convenient methods to set a request matcher to determine whether a request requires CSP protection or not. Closes spring-projectsgh-10826 Signed-off-by: Ziqin Wang <ziqin@wangziqin.net>
|
|
||
| @Override | ||
| public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { | ||
| return Mono.fromSupplier(this.nonceGenerator::generateKey).flatMap((nonce) -> { |
There was a problem hiding this comment.
Let's please place the Mono directly into the exchange as an attribute. This will allow so that only when the attribute is read that we have the expense of generating a nonce. You can see ServerCsrfTokenRequestAttributeHandler for an example.
There was a problem hiding this comment.
Lazy generation of nonce is beneficial for performance. Thank you for pointing out this aspect which I didn't take into account. I have updated the code accordingly in 56e3884 with SingletonSupplier and Mono#cache() so that the nonce used by views is the same as the one appearing in the CSP header.
Nevertheless, I still have a concern on how to access the nonce in a template. If I remember correctly, in WebFlux applications a Mono<String> can be directly accessed in a Thymeleaf template like <style th:nonce="${_csp_nonce}">, but I doubt whether there is similar support for Supplier<String> in servlet applications.
Is there anything we can do to avoid having users to write different templates for different stacks?
There was a problem hiding this comment.
A possible solution could be ${_csp.nonce}, where the request attribute named _csp is an object with a getNonce() method performing the lazy evaluation, just like SupplierCsrfToken#getToken().
There was a problem hiding this comment.
Another possible approach is to hack the toString() method of the Supplier<String> instance to perform the deferred generation.
| public ContentSecurityPolicySpec requireCspMatcher(ServerWebExchangeMatcher matcher) { | ||
| Assert.notNull(matcher, "Matcher must not be null"); | ||
| // Replace the CSP writer in the list with a matcher-decorated writer | ||
| int idx = HeaderSpec.this.writers.indexOf(HeaderSpec.this.contentSecurityPolicy); |
There was a problem hiding this comment.
Let's instead keep track of the request matcher(s) inside of ContentSecurityPolicySpec and apply them when the filter chain is being built.
There was a problem hiding this comment.
To avoid the hack here, the initialization of this.writers in HeaderSpec constructor is moved to HeaderSpec#configure, which requires me to change the disable() method implementations of other XxxSpecs. See my comment for how theses changes may affect some weird applications.
This is updated in acf956a. Could you please check whether these changes are appropriate?
This reverts commit 381dc38. Signed-off-by: Ziqin Wang <ziqin@wangziqin.net>
Signed-off-by: Ziqin Wang <ziqin@wangziqin.net>
Signed-off-by: Ziqin Wang <ziqin@wangziqin.net>
Signed-off-by: Ziqin Wang <ziqin@wangziqin.net>
Signed-off-by: Ziqin Wang <ziqin@wangziqin.net>
Signed-off-by: Ziqin Wang <ziqin@wangziqin.net>
To align with the rest of the DSL Signed-off-by: Ziqin Wang <ziqin@wangziqin.net>
Signed-off-by: Ziqin Wang <ziqin@wangziqin.net>
Signed-off-by: Ziqin Wang <ziqin@wangziqin.net>
Signed-off-by: Ziqin Wang <ziqin@wangziqin.net>
Replaced by `ContentSecurityPolicySpec#reportOnly()`. The return type is changed to ContentSecurityPolicySpec to allow method chaining in lambda DSL. Signed-off-by: Ziqin Wang <ziqin@wangziqin.net>
Replaced by `ContentSecurityPolicySpec#directives(String)`. The return type is changed to ContentSecurityPolicySpec to allow method chaining in lambda DSL. Signed-off-by: Ziqin Wang <ziqin@wangziqin.net>
| public HeaderSpec disable() { | ||
| HeaderSpec.this.writers.remove(HeaderSpec.this.frameOptions); | ||
| HeaderSpec.this.frameOptions = null; | ||
| return HeaderSpec.this; | ||
| } |
There was a problem hiding this comment.
If someone has written some weird code like below, the changes here could cause NullPointerException which the old code won't throw.
@Bean
SecurityWebFilterChain springSecurity(ServerHttpSecurity http) {
http
.headers((headers) -> headers
.frameOptions((frame) -> {
frame.disable();
frame.mode(XFrameOptionsServerHttpHeadersWriter.Mode.DENY);
})
);
return http.build();
}|
@jzheaux Thank you for your detailed and insightful review feedback! I have resolved most problems you pointed out, but there are still some points that I am uncertain about (see my comments for details). Would you like to help me about these? |
| HttpHeaderWriterWebFilter result = new HttpHeaderWriterWebFilter(writer); | ||
| http.addFilterAt(result, SecurityWebFiltersOrder.HTTP_HEADERS_WRITER); | ||
| http.addFilterBefore(this.contentSecurityPolicy.getNonceGeneratingFilter(), | ||
| SecurityWebFiltersOrder.HTTP_HEADERS_WRITER); |
There was a problem hiding this comment.
Should we define a new SecurityWebFiltersOrder enum for ContentSecurityPolicyNonceGeneratingWebFilter? In SecurityWebFiltersOrder, is it a good idea to expose the presence of ContentSecurityPolicyNonceGeneratingWebFilter to users considering that it may not be necessary in the future if headers are written eagerly?
This PR implements gh-10826.
Introduction
When strict Content Security Policy is used, web browsers block inline
<script>or<style>blocks in HTML to mitigate XSS attacks injecting malicious inline blocks. To allow intended inline blocks, web developers can generate a hard-to-guess nonce and specify it in both the CSP and allowed inline blocks.Currently, Spring Security only supports specifying static content security policy directives. This PR introduces support for dynamically generating a secure random nonce for CSP.
Implementation
A nonce is written to the CSP header in 2 steps:
NonceGeneratingFilter/NonceGeneratingWebFiltergenerates a nonce and set it as a request attribute named_csp_nonce;ContentSecurityPolicyHeaderWriter/ContentSecurityPolicyServerHttpHeadersWriterreads the_csp_nonceattribute and write it to the CSP header, replacing the{nonce}placeholder in the configuredpolicyDirectives.Note:
_csp_nonceis chosen as the default attribute name because it has a similar format with the existing_csrfattribute. The attribute name is configurable.Configuration
This PR also adds configurers for setting up nonce-based CSP with Java/Kotlin lambda DSL, including the ability to specify a request matcher to determine whether a request requires CSP protection.
The ability to enable CSP conditionally is useful especially when the CSP directives contain a nonce, because the HTTP header value becomes dynamic and may change the cacheability of static asserts protected by Spring Security.
The conditional enabling of CSP protection is implemented in
spring-security-configby wrapping theContentSecurityPolicyHeaderWriter/ContentSecurityPolicyServerHttpHeadersWriterwith the existingDelegatingRequestMatcherHeaderWriter/ServerWebExchangeDelegatingServerHttpHeadersWriter. The_csp_nonceattribute is generated unconditionally if nonce-based CSP is configured.Breaking changes in corner case
In commit 381dc38 I changed the return type of 2 public configuration APIs:
ContentSecurityPolicySpec#reportOnly(boolean)ContentSecurityPolicySpec#policyDirectives(String)The return type is changed from
HeaderSpectoContentSecurityPolicySpecto allow method chaining.I believe this API change was missed during the migration to lambda DSL and think it's small enough to be updated when releasing v7.1, but if 100% API stability is required, we could simply revert that commit, or introduce new APIs and deprecate the old ones.