Repository navigation
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, |
00533a5 to
0ecdc80
Compare
|
@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? |
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!
|
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.
|
|
Thanks again for all the work on this, @ziqin, and for your patience through the review rounds. To help get this over the line before I'm away next week, I've pushed a handful of polish commits to the branch. They're intentionally separate from yours so you can see exactly what changed, and please push back as needed; each can be dropped or reworked independently. What I changed and why:
I'll resolve the review threads that these commits (and yours) have settled. I'd like to merge on Friday so it lands before I'm out. If you'd like any of the above reverted, or want more time, just say so and I'll wait. I'll add the reference documentation as a follow-on polish. I've also filed a few follow-ups separately, and you're welcome to pick any of them up:
If I don't hear from you on any of those in the next few days, I'll add the |
|
Regarding your comment about #18840: Even though #18840 changes the header eagerness default, folks can still change to be lazy if they need that. Because of that, we need to support lazy headers regardless of #18840. |
I built an WebFlux + Thymeleaf sample and also confirmed that a Spring WebFlux document mentions support for reactive model attribute:
But exchange attribute is not covered. I am sorry for saying “Now both Spring MVC and WebFlux applications can render the nonce with a template like |
| .requestMatchers("/foo/**")) | ||
| .contentSecurityPolicy((csp) -> csp | ||
| .policyDirectives("default-src 'self'") | ||
| .requestMatchers("/bar/**"))); |
There was a problem hiding this comment.
If gh-19842 is supported, the configureWhenContentSecurityPolicyConfiguredTwiceThenSecondCallReplacesFirst test will fail because ContentSecurityPolicyConfig#requestMatchers(String...) rejects the second call.
This behavior is introduced to (and documented in) requestMatcher(RequestMatcher) and requestMatchers(String...) to prevent a previous requestMatchers(String...) call from being accidentally overridden by a consecutive requestMatcher(RequestMatcher) call (or vice versa) in the same contentSecurityPolicy(Customizer) call.
There was a problem hiding this comment.
I believe it is fine to omit the first requestMatchers("/foo/**") here for the purpose of the test, so that we don't commit to any specific behavior through the tests (neither replacing everything nor retaining existing configurations).
@Configuration
@EnableWebSecurity
static class ContentSecurityPolicyConfiguredTwiceConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) {
// @formatter:off
http
.headers((headers) -> headers
.defaultsDisabled()
.contentSecurityPolicy((csp) -> csp
.policyDirectives("script-src 'self'"))
.contentSecurityPolicy((csp) -> csp
.policyDirectives("default-src 'self'")
.requestMatchers("/bar/**")));
// @formatter:on
return http.build();
}
}For reactive stack:
@Test
public void headersWhenContentSecurityPolicyConfiguredTwiceThenSecondCallReplacesFirst() {
String headerName = ContentSecurityPolicyServerHttpHeadersWriter.CONTENT_SECURITY_POLICY;
String policyDirectives = "default-src 'self'";
// @formatter:off
this.http.headers((headers) -> headers
.contentSecurityPolicy((csp) -> csp
.policyDirectives("script-src 'self'"))
.contentSecurityPolicy((csp) -> csp
.exchangeMatchers("/bar/**")
.policyDirectives(policyDirectives)));
// @formatter:on
WebTestClient client = WebTestClientBuilder.bindToWebFilters(this.http.build()).build();
// @formatter:off
client.get()
.uri("https://example.com/bar")
.exchange()
.expectHeader().valueEquals(headerName, policyDirectives);
client.get()
.uri("https://example.com/foo")
.exchange()
.expectHeader().doesNotExist(headerName);
// @formatter:on
}There was a problem hiding this comment.
I have added the above changes to ziqin@1e81fd0
There was a problem hiding this comment.
Thanks for noticing this and for thinking ahead! Actually, I think we should address this in another way and I've added 64ee4eb just now.
While we do want the DSL to be able to compose at the configurer level, it conventionally allows individual fields to override previous calls.
When a strict Content Security Policy is used, browsers block inline
<script> and <style> blocks to mitigate XSS. To allow intended inline
blocks, applications can generate a hard-to-guess nonce and include it
in both the policy and the allowed inline blocks.
This commit adds ContentSecurityPolicyNonceGeneratingFilter and
ContentSecurityPolicyNonceGeneratingWebFilter, which lazily generate a
nonce and expose it as a request attribute for views to render. The
CSP header writers replace the {nonce} placeholder in the configured
directives with that nonce.
It also adds Java and Kotlin DSL support for configuring nonce-based
CSP, including a request matcher for deciding which requests require
CSP, and changes the composite header writers to avoid failing fast.
Closes spring-projectsgh-10826
Signed-off-by: Ziqin Wang <[email protected]>
Since the nonce-generating filter does not yet support reactive sources, we complicate the request attribute value by placing a Mono inside of it. In the future, if we do support reactive sources, folks will set their own attribute once they resolve the Mono in something like a ControllerAdvice Signed-off-by: Josh Cummings <[email protected]>
Signed-off-by: Josh Cummings <[email protected]>
Upon further review, I don't think that we should support this at this time. XML is effectively deprecated and we aren't adding more features unless it comes for free. Signed-off-by: Josh Cummings <[email protected]>
I'm not yet convinced that this is the right interface to expose for nonce configuration. Also, it's just as easy to publish a new filter than configure this one, so this commit leaves it out for now. Signed-off-by: Josh Cummings <[email protected]>
Signed-off-by: Josh Cummings <[email protected]>
Since the most common case is that there will be just one exception, this commit propagates that one, suppressing any additional Signed-off-by: Josh Cummings <[email protected]>
ContentSecurityPolicySpec(String) has no callers, and it would not have applied the defaults that the no-arg constructor now sets up, like the nonce filter and the exchange matcher. Thanks to Ziqin Wang for pointing this out in review. Signed-off-by: Josh Cummings <[email protected]>
Consistent with other DSL methods like HstsConfig#requestMatcher, the last call to requestMatcher(s) or exchangeMatcher(s) now wins instead of failing. Signed-off-by: Josh Cummings <[email protected]>
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.