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>
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>
|
@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?
jzheaux
left a comment
There was a problem hiding this comment.
Thanks, @ziqin, for this thorough PR!
In addition to my inline notes, will you please take a look at XML support? We can either:
- Add the nonce-generating filter there too so that the XML support continues to work, or
- It will need an error message stating that nonces aren't supported in XML config
Either way, no need to add nonce-attribute-name, etc. This is to make sure that this change doesn't inadvertently break XML config.
Additionally, for brevity, I made some comments in servlet that naturally also apply to reactive. If you are unsure, please feel free to ask!
|
|
||
| private final ContentSecurityPolicyServerHttpHeadersWriter writer = new ContentSecurityPolicyServerHttpHeadersWriter(); | ||
|
|
||
| private @Nullable String nonceAttributeName; |
There was a problem hiding this comment.
I lean towards giving this a default value as having a nonce is the stronger security position. It also prevents folks from having to specify an attribute name that Spring Security already has defined.
There was a problem hiding this comment.
Having a null nonceAttributeName here in ContentSecurityPolicySpec means using the default attribute name _csp_nonce. (Now the default name is _csp.) When it's not null, it would be passed to ContentSecurityPolicyNonceGeneratingWebFilter#setAttributeName(String), otherwise the filter will use the default attribute name.
In 6c1fbf3, this nonceAttributeName field is removed from ContentSecurityPolicySpec together with the removal of ContentSecurityPolicySpec#getNonceGeneratingFilter(). Now ContentSecurityPolicySpec#nonceAttributeName(String) directly configures the pre-constructed HeaderSpec#nonceGeneratingFilter. I believe such changes simplify the code and reduce changes in this PR.
| */ | ||
| public ContentSecurityPolicyConfig nonceAttributeName(String nonceAttributeName) { | ||
| Assert.hasLength(nonceAttributeName, "NonceAttributeName must not be null or empty"); | ||
| this.nonceAttributeName = nonceAttributeName; |
There was a problem hiding this comment.
Because ContentSecurityPolicyHeaderWriter is reconstructed each time headers.contentSecurityPolicy is invoked, there is a lifecycle mismatch between nonceAttributeName/requestMatcher and reportOnly/policyDirectives.
Let's please see if we can set these values eagerly as well. We can use another ticket to see change the fact that a second call to contentSecurityPolicy clobbers the first.
There was a problem hiding this comment.
In 6c1fbf3, ContentSecurityPolicyConfig#requestMatcher(RequestMatcher) now directly configures on ContentSecurityPolicyConfig.this.writer, just like reportOnly and policyDirectives.
Similarly, ContentSecurityPolicyConfig#nonceAttributeName(String) now directly configures on ContentSecurityPolicyConfig.this.nonceGeneratingFilter.
Thus, I believe the lifecycle of these properties should be the same now.
The problem that a second call to contentSecurityPolicy clobbers the first could be addressed with the following solution, which is applied by many other inner config classes in HeadersConfigurer:
// 1. Add an enable() method to class ContentSecurityPolicyConfig
private ContentSecurityPolicyConfig enable() {
if (this.writer == null) {
this.writer = new ContentSecurityPolicyHeaderWriter();
}
if (this.nonceGeneratingFilter == null) {
this.nonceGeneratingFilter = new ContentSecurityPolicyNonceGeneratingFilter();
}
return this;
}
// 2. Call contentSecurityPolicy.enable() in HeadersConfigurer#contentSecurityPolicy(Customizer)
// to preserve already constructed writer and nonceGeneratingFilter
public HeadersConfigurer<H> contentSecurityPolicy(
Customizer<ContentSecurityPolicyConfig> contentSecurityCustomizer) {
contentSecurityCustomizer.customize(this.contentSecurityPolicy.enable());
return HeadersConfigurer.this;
}I've added the above changes to a new branch.
| } | ||
|
|
||
| ContentSecurityPolicyNonceGeneratingFilter getNonceGeneratingFilter() { | ||
| var filter = new ContentSecurityPolicyNonceGeneratingFilter(); |
There was a problem hiding this comment.
I think it would be okay to default to having this be a pre-constructed field in the config. If they set nonceAttributeName to null, then that filter can be set to null.
There was a problem hiding this comment.
In 6c1fbf3, ContentSecurityPolicyNonceGeneratingFilter becomes a field in ContentSecurityPolicyConfig. Currently it is pre-constructed by HeadersConfigurer#contentSecurityPolicy(Customizer), but is expected to be constructed by a separate enable() method. See the discussion in #18499 (comment).
I don't understand neither why a user might want to set nonceAttributeName to null, nor the semantic of a null nonceAttributeName. From my point of view, if a nonce is generated and written to the Content-Security-Policy header but cannot be referenced by any inline blocks in server-rendered HTML, the nonce is useless and the user had better remove that {nonce} placeholder from the policy directives. The current implementation of ContentSecurityPolicyConfig#nonceAttributeName(String) rejects null argument.
| Mono<String> deferredNonce = exchange | ||
| .getAttribute(ContentSecurityPolicyNonceGeneratingWebFilter.class.getName()); | ||
| if (deferredNonce == null) { | ||
| return Mono.error(new IllegalStateException( |
There was a problem hiding this comment.
I believe this is correct. Also, this means that CompositeServerHttpHeadersWriter will fail fast, and the ensuing headers will not be written.
If you are able, can you please add a separate commit for updating CompositeServerHttpHeadersWriter to not fail fast? I believe it would mean changing the concatMap call to concatMapDelayError.
Signed-off-by: Ziqin Wang <ziqin@wangziqin.net>
Now that ServerWebExchangeMatcher is moved into ContentSecurityPolicyServerHttpHeadersWriter, the CSP header writer could be eagerly constructed and placed in HeaderSpec directly, just like other header writers. Thus, changes about header writer disabling and composition introduced in acf956a are no longer necessary. 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>
Signed-off-by: Ziqin Wang <ziqin@wangziqin.net>
Signed-off-by: Ziqin Wang <ziqin@wangziqin.net>
|
Thanks again for your patient review. Following your guidance, I have resolved most problems. Please see my replies to inline comments for details and my remaining questions.
|
Signed-off-by: Ziqin Wang <ziqin@wangziqin.net>
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.