Skip to content

Commit 36ac5aa

Browse files
committed
Support configuring nonce-based Content-Security-Policy
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 gh-10826 Signed-off-by: Ziqin Wang <ziqin@wangziqin.net>
1 parent dc112ee commit 36ac5aa

8 files changed

Lines changed: 974 additions & 8 deletions

File tree

config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,29 @@
1818

1919
import java.net.URI;
2020
import java.util.ArrayList;
21+
import java.util.Arrays;
2122
import java.util.List;
2223
import java.util.Map;
24+
import java.util.function.Function;
2325

2426
import jakarta.servlet.http.HttpServletRequest;
2527

28+
import org.jspecify.annotations.Nullable;
29+
2630
import org.springframework.security.config.Customizer;
2731
import org.springframework.security.config.annotation.configuration.ObjectPostProcessorConfiguration;
2832
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
2933
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
3034
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
3135
import org.springframework.security.web.header.HeaderWriter;
3236
import org.springframework.security.web.header.HeaderWriterFilter;
37+
import org.springframework.security.web.header.NonceGeneratingFilter;
3338
import org.springframework.security.web.header.writers.CacheControlHeadersWriter;
3439
import org.springframework.security.web.header.writers.ContentSecurityPolicyHeaderWriter;
3540
import org.springframework.security.web.header.writers.CrossOriginEmbedderPolicyHeaderWriter;
3641
import org.springframework.security.web.header.writers.CrossOriginOpenerPolicyHeaderWriter;
3742
import org.springframework.security.web.header.writers.CrossOriginResourcePolicyHeaderWriter;
43+
import org.springframework.security.web.header.writers.DelegatingRequestMatcherHeaderWriter;
3844
import org.springframework.security.web.header.writers.FeaturePolicyHeaderWriter;
3945
import org.springframework.security.web.header.writers.HpkpHeaderWriter;
4046
import org.springframework.security.web.header.writers.HstsHeaderWriter;
@@ -45,6 +51,8 @@
4551
import org.springframework.security.web.header.writers.XXssProtectionHeaderWriter;
4652
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter;
4753
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter.XFrameOptionsMode;
54+
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
55+
import org.springframework.security.web.util.matcher.OrRequestMatcher;
4856
import org.springframework.security.web.util.matcher.RequestMatcher;
4957
import org.springframework.util.Assert;
5058

@@ -273,6 +281,14 @@ public HeadersConfigurer<H> defaultsDisabled() {
273281
public void configure(H http) {
274282
HeaderWriterFilter headersFilter = createHeaderWriterFilter();
275283
http.addFilter(headersFilter);
284+
configureCspNonceGeneratingFilter(http);
285+
}
286+
287+
private void configureCspNonceGeneratingFilter(H http) {
288+
ContentSecurityPolicyHeaderWriter writer = this.contentSecurityPolicy.writer;
289+
if (writer != null && writer.isNonceBased()) {
290+
http.addFilterBefore(new NonceGeneratingFilter(writer.getNonceAttributeName()), HeaderWriterFilter.class);
291+
}
276292
}
277293

278294
/**
@@ -302,7 +318,7 @@ private List<HeaderWriter> getHeaderWriters() {
302318
addIfNotNull(writers, this.hsts.writer);
303319
addIfNotNull(writers, this.frameOptions.writer);
304320
addIfNotNull(writers, this.hpkp.writer);
305-
addIfNotNull(writers, this.contentSecurityPolicy.writer);
321+
addIfNotNull(writers, this.contentSecurityPolicy.getWriter());
306322
addIfNotNull(writers, this.referrerPolicy.writer);
307323
addIfNotNull(writers, this.featurePolicy.writer);
308324
addIfNotNull(writers, this.permissionsPolicy.writer);
@@ -937,11 +953,15 @@ public final class ContentSecurityPolicyConfig {
937953

938954
private ContentSecurityPolicyHeaderWriter writer;
939955

956+
private @Nullable RequestMatcher requestMatcher;
957+
940958
private ContentSecurityPolicyConfig() {
941959
}
942960

943961
/**
944-
* Sets the security policy directive(s) to be used in the response header.
962+
* Sets the security policy directive(s) to be used in the response header. The
963+
* {@code policyDirectives} may contain {@code {nonce}} as placeholders for a
964+
* generated secure random nonce, e.g., {@code script-src 'self' 'nonce-{nonce}'}.
945965
* @param policyDirectives the security policy directive(s)
946966
* @return the {@link ContentSecurityPolicyConfig} for additional configuration
947967
* @throws IllegalArgumentException if policyDirectives is null or empty
@@ -961,6 +981,70 @@ public ContentSecurityPolicyConfig reportOnly() {
961981
return this;
962982
}
963983

984+
/**
985+
* Sets the name of the servlet request attribute for the generated nonce. Views
986+
* can read this attribute to render the nonce in HTML.
987+
* @param nonceAttributeName the name of the nonce attribute
988+
* @return the {@link ContentSecurityPolicyConfig} for additional configuration
989+
* @throws IllegalArgumentException if {@code nonceAttributeName} is {@code null}
990+
* or empty
991+
* @since 7.1
992+
*/
993+
public ContentSecurityPolicyConfig nonceAttributeName(String nonceAttributeName) {
994+
this.writer.setNonceAttributeName(nonceAttributeName);
995+
return this;
996+
}
997+
998+
/**
999+
* Specifies the {@link RequestMatcher} to use for determining when CSP should be
1000+
* applied. The default is to enable CSP in every response if
1001+
* {@link HeadersConfigurer#contentSecurityPolicy(Customizer)} is configured.
1002+
* @param requestMatcher the {@link RequestMatcher} to use
1003+
* @return the {@link ContentSecurityPolicyConfig} for additional configuration
1004+
* @throws IllegalArgumentException if {@code requestMatcher} is null
1005+
* @throws IllegalStateException if a {@link RequestMatcher} is already configured
1006+
* by a previous call of this method or {@link #requireCspMatchers(String...)}
1007+
* @since 7.1
1008+
* @see #requireCspMatchers(String...)
1009+
*/
1010+
public ContentSecurityPolicyConfig requireCspMatcher(RequestMatcher requestMatcher) {
1011+
Assert.notNull(requestMatcher, "RequestMatcher cannot be null");
1012+
Assert.state(this.requestMatcher == null, "RequireCspMatcher(s) is already configured");
1013+
this.requestMatcher = requestMatcher;
1014+
return this;
1015+
}
1016+
1017+
/**
1018+
* Specifies the matching path patterns for determining when CSP should be
1019+
* applied. The default is to enable CSP in every response if
1020+
* {@link HeadersConfigurer#contentSecurityPolicy(Customizer)} is configured.
1021+
* @param pathPatterns the path patterns to be matched with a
1022+
* {@link PathPatternRequestMatcher}
1023+
* @return the {@link ContentSecurityPolicyConfig} for additional configuration
1024+
* @throws IllegalArgumentException if any path pattern if rejected by
1025+
* {@link PathPatternRequestMatcher.Builder#matcher(String)}
1026+
* @throws IllegalStateException if a {@link RequestMatcher} is already configured
1027+
* by a previous call of this method or {@link #requireCspMatcher(RequestMatcher)}
1028+
* @since 7.1
1029+
* @see #requireCspMatcher(RequestMatcher)
1030+
*/
1031+
public ContentSecurityPolicyConfig requireCspMatchers(String... pathPatterns) {
1032+
PathPatternRequestMatcher.Builder builder = HeadersConfigurer.this.getRequestMatcherBuilder();
1033+
// @formatter:off
1034+
return this.requireCspMatcher(new OrRequestMatcher(
1035+
Arrays.stream(pathPatterns)
1036+
.map((Function<? super String, RequestMatcher>) builder::matcher)
1037+
.toList()));
1038+
// @formatter:on
1039+
}
1040+
1041+
HeaderWriter getWriter() {
1042+
if (this.requestMatcher != null) {
1043+
return new DelegatingRequestMatcherHeaderWriter(this.requestMatcher, this.writer);
1044+
}
1045+
return this.writer;
1046+
}
1047+
9641048
}
9651049

9661050
public final class ReferrerPolicyConfig {

config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,10 +193,12 @@
193193
import org.springframework.security.web.server.header.CrossOriginResourcePolicyServerHttpHeadersWriter.CrossOriginResourcePolicy;
194194
import org.springframework.security.web.server.header.FeaturePolicyServerHttpHeadersWriter;
195195
import org.springframework.security.web.server.header.HttpHeaderWriterWebFilter;
196+
import org.springframework.security.web.server.header.NonceGeneratingWebFilter;
196197
import org.springframework.security.web.server.header.PermissionsPolicyServerHttpHeadersWriter;
197198
import org.springframework.security.web.server.header.ReferrerPolicyServerHttpHeadersWriter;
198199
import org.springframework.security.web.server.header.ReferrerPolicyServerHttpHeadersWriter.ReferrerPolicy;
199200
import org.springframework.security.web.server.header.ServerHttpHeadersWriter;
201+
import org.springframework.security.web.server.header.ServerWebExchangeDelegatingServerHttpHeadersWriter;
200202
import org.springframework.security.web.server.header.StrictTransportSecurityServerHttpHeadersWriter;
201203
import org.springframework.security.web.server.header.XFrameOptionsServerHttpHeadersWriter;
202204
import org.springframework.security.web.server.header.XXssProtectionServerHttpHeadersWriter;
@@ -2560,6 +2562,10 @@ protected void configure(ServerHttpSecurity http) {
25602562
ServerHttpHeadersWriter writer = new CompositeServerHttpHeadersWriter(this.writers);
25612563
HttpHeaderWriterWebFilter result = new HttpHeaderWriterWebFilter(writer);
25622564
http.addFilterAt(result, SecurityWebFiltersOrder.HTTP_HEADERS_WRITER);
2565+
if (this.contentSecurityPolicy.isNonceBased()) {
2566+
http.addFilterBefore(new NonceGeneratingWebFilter(this.contentSecurityPolicy.getNonceAttributeName()),
2567+
SecurityWebFiltersOrder.HTTP_HEADERS_WRITER);
2568+
}
25632569
}
25642570

25652571
/**
@@ -2854,6 +2860,9 @@ public ContentSecurityPolicySpec reportOnly(boolean reportOnly) {
28542860

28552861
/**
28562862
* Sets the security policy directive(s) to be used in the response header.
2863+
* The {@code policyDirectives} may contain {@code {nonce}} as placeholders
2864+
* for a generated secure random nonce, e.g., {@code script-src 'self'
2865+
* 'nonce-{nonce}'}.
28572866
* @param policyDirectives the security policy directive(s)
28582867
* @return the {@link ContentSecurityPolicySpec} to continue configuring
28592868
*/
@@ -2862,6 +2871,63 @@ public ContentSecurityPolicySpec policyDirectives(String policyDirectives) {
28622871
return this;
28632872
}
28642873

2874+
/**
2875+
* Sets the name of the {@link ServerWebExchange#getAttribute(String) request
2876+
* attribute} for the generated nonce. Views can read this attribute to render
2877+
* the nonce in HTML.
2878+
* @param nonceAttributeName the name of the nonce attribute
2879+
* @return the {@link ContentSecurityPolicySpec} to continue configuring
2880+
* @throws IllegalArgumentException if {@code nonceAttributeName} is
2881+
* {@code null} or empty
2882+
* @since 7.1
2883+
*/
2884+
public ContentSecurityPolicySpec nonceAttributeName(String nonceAttributeName) {
2885+
HeaderSpec.this.contentSecurityPolicy.setNonceAttributeName(nonceAttributeName);
2886+
return this;
2887+
}
2888+
2889+
/**
2890+
* Specifies the {@link ServerWebExchangeMatcher} to use for determining when
2891+
* CSP should be applied. The default is to enable CSP in every response if
2892+
* {@link HeaderSpec#contentSecurityPolicy(Customizer)} is configured.
2893+
* @param matcher the {@link ServerWebExchangeMatcher} to use
2894+
* @return the {@link ContentSecurityPolicySpec} to continue configuring
2895+
* @throws IllegalArgumentException if {@code matcher} is {@code null}
2896+
* @throws IllegalStateException if a {@link ServerWebExchangeMatcher} is
2897+
* already configured by a previous call of this method or
2898+
* {@link #requireCspMatchers(String...)}
2899+
* @since 7.1
2900+
* @see #requireCspMatchers(String...)
2901+
*/
2902+
public ContentSecurityPolicySpec requireCspMatcher(ServerWebExchangeMatcher matcher) {
2903+
Assert.notNull(matcher, "Matcher must not be null");
2904+
// Replace the CSP writer in the list with a matcher-decorated writer
2905+
int idx = HeaderSpec.this.writers.indexOf(HeaderSpec.this.contentSecurityPolicy);
2906+
Assert.state(idx >= 0, "RequireCspMatcher(s) is already configured");
2907+
HeaderSpec.this.writers.set(idx, new ServerWebExchangeDelegatingServerHttpHeadersWriter(matcher,
2908+
HeaderSpec.this.contentSecurityPolicy));
2909+
return this;
2910+
}
2911+
2912+
/**
2913+
* Specifies the matching path patterns for determining when CSP should be
2914+
* applied. The default is to enable CSP in every response if
2915+
* {@link HeaderSpec#contentSecurityPolicy(Customizer)} is configured.
2916+
* @param pathPatterns the path patterns to be matched with a
2917+
* {@link PathPatternParserServerWebExchangeMatcher}
2918+
* @return the {@link ContentSecurityPolicySpec} to continue configuring
2919+
* @throws IllegalArgumentException if any path pattern is rejected by
2920+
* {@link PathPatternParserServerWebExchangeMatcher}
2921+
* @throws IllegalStateException if a {@link ServerWebExchangeMatcher} is
2922+
* already configured by a previous call of this method or
2923+
* {@link #requireCspMatcher(ServerWebExchangeMatcher)}
2924+
* @since 7.1
2925+
* @see #requireCspMatcher(ServerWebExchangeMatcher)
2926+
*/
2927+
public ContentSecurityPolicySpec requireCspMatchers(String... pathPatterns) {
2928+
return this.requireCspMatcher(ServerWebExchangeMatchers.pathMatchers(pathPatterns));
2929+
}
2930+
28652931
private ContentSecurityPolicySpec(String policyDirectives) {
28662932
HeaderSpec.this.contentSecurityPolicy.setPolicyDirectives(policyDirectives);
28672933
}

config/src/main/kotlin/org/springframework/security/config/annotation/web/headers/ContentSecurityPolicyDsl.kt

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,63 @@ package org.springframework.security.config.annotation.web.headers
1818

1919
import org.springframework.security.config.annotation.web.builders.HttpSecurity
2020
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer
21+
import org.springframework.security.web.util.matcher.RequestMatcher
2122

2223
/**
2324
* A Kotlin DSL to configure the [HttpSecurity] Content-Security-Policy header using
2425
* idiomatic Kotlin code.
2526
*
2627
* @author Eleftheria Stein
28+
* @author Ziqin Wang
2729
* @since 5.3
28-
* @property policyDirectives the security policy directive(s) to be used in the response header.
29-
* @property reportOnly includes the Content-Security-Policy-Report-Only header in the response.
3030
*/
3131
@HeadersSecurityMarker
3232
class ContentSecurityPolicyDsl {
33+
/**
34+
* The security policy directive(s) to be used in the response header.
35+
* The [policyDirectives] may contain `{code}` as placeholders for a generated secure
36+
* random nonce, e.g., `script-src 'self' 'nonce-{nonce}'`.
37+
*/
3338
var policyDirectives: String? = null
39+
40+
/** Includes the `Content-Security-Policy-Report-Only` header in the response. */
3441
var reportOnly: Boolean? = null
3542

43+
/**
44+
* The name of the servlet request attribute for the generated nonce. Views can read
45+
* this attribute to render the nonce in HTML.
46+
* @since 7.1
47+
*/
48+
var nonceAttributeName: String? = null
49+
50+
/**
51+
* The [RequestMatcher] to use for determining when CSP should be applied.
52+
* The default is to enable CSP in every response if
53+
* [org.springframework.security.config.annotation.web.HeadersDsl.contentSecurityPolicy]
54+
* is configured.
55+
* You can configure either this property or [requireCspMatchers], but not both.
56+
* @since 7.1
57+
* @see requireCspMatchers
58+
*/
59+
var requireCspMatcher: RequestMatcher? = null
60+
61+
private var requireCspPathPatterns: Array<out String>? = null
62+
63+
/**
64+
* Specify the matching path patterns for determining when CSP should be applied.
65+
* The default is to write CSP header in every response if
66+
* [org.springframework.security.config.annotation.web.HeadersDsl.contentSecurityPolicy]
67+
* is configured.
68+
* You can configure either this method or [requireCspMatcher], but not both.
69+
* @param pathPatterns the path patterns to be matched with a
70+
* [org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher]
71+
* @since 7.1
72+
* @see requireCspMatcher
73+
*/
74+
fun requireCspMatchers(vararg pathPatterns: String) {
75+
requireCspPathPatterns = pathPatterns
76+
}
77+
3678
internal fun get(): (HeadersConfigurer<HttpSecurity>.ContentSecurityPolicyConfig) -> Unit {
3779
return { contentSecurityPolicy ->
3880
policyDirectives?.also {
@@ -43,6 +85,9 @@ class ContentSecurityPolicyDsl {
4385
contentSecurityPolicy.reportOnly()
4486
}
4587
}
88+
nonceAttributeName?.also(contentSecurityPolicy::nonceAttributeName)
89+
requireCspMatcher?.also(contentSecurityPolicy::requireCspMatcher)
90+
requireCspPathPatterns?.also(contentSecurityPolicy::requireCspMatchers)
4691
}
4792
}
4893
}

config/src/main/kotlin/org/springframework/security/config/web/server/ServerContentSecurityPolicyDsl.kt

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,61 @@
1616

1717
package org.springframework.security.config.web.server
1818

19+
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher
20+
1921
/**
2022
* A Kotlin DSL to configure the [ServerHttpSecurity] Content-Security-Policy header using
2123
* idiomatic Kotlin code.
2224
*
2325
* @author Eleftheria Stein
26+
* @author Ziqin Wang
2427
* @since 5.4
2528
*/
2629
@ServerSecurityMarker
2730
class ServerContentSecurityPolicyDsl {
31+
/**
32+
* The security policy directive(s) to be used in the response header.
33+
* The `policyDirectives` may contain `{nonce}` as placeholders for a generated
34+
* secure random nonce, e.g., `script-src 'self' 'nonce-{nonce}'`.
35+
*/
2836
var policyDirectives: String? = null
37+
38+
/** Includes the `Content-Security-Policy-Report-Only` header in the response. */
2939
var reportOnly: Boolean? = null
3040

41+
/**
42+
* The name of the request attribute for the generated nonce. Views can read this
43+
* attribute to render the nonce in HTML.
44+
* @since 7.1
45+
*/
46+
var nonceAttributeName: String? = null
47+
48+
/**
49+
* The [ServerWebExchangeMatcher] to use for determining when CSP should be applied.
50+
* The default is to enable CSP in every response if [ServerHeadersDsl.contentSecurityPolicy]
51+
* is configured.
52+
* You can configure either this property or [requireCspMatchers], but not both.
53+
* @since 7.1
54+
* @see requireCspMatchers
55+
*/
56+
var requireCspMatcher: ServerWebExchangeMatcher? = null
57+
58+
private var requireCspPathPatterns: Array<out String>? = null
59+
60+
/**
61+
* Specify the matching path patterns for determining when CSP should be applied.
62+
* The default is to enable CSP in every response if [ServerHeadersDsl.contentSecurityPolicy]
63+
* is configured.
64+
* You can configure either this method or [requireCspMatcher], but not both.
65+
* @param pathPatterns the path patterns to be matched with a
66+
* [org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher]
67+
* @since 7.1
68+
* @see requireCspMatcher
69+
*/
70+
fun requireCspMatchers(vararg pathPatterns: String) {
71+
requireCspPathPatterns = pathPatterns
72+
}
73+
3174
internal fun get(): (ServerHttpSecurity.HeaderSpec.ContentSecurityPolicySpec) -> Unit {
3275
return { contentSecurityPolicy ->
3376
policyDirectives?.also {
@@ -36,6 +79,9 @@ class ServerContentSecurityPolicyDsl {
3679
reportOnly?.also {
3780
contentSecurityPolicy.reportOnly(reportOnly!!)
3881
}
82+
nonceAttributeName?.also(contentSecurityPolicy::nonceAttributeName)
83+
requireCspMatcher?.also(contentSecurityPolicy::requireCspMatcher)
84+
requireCspPathPatterns?.also(contentSecurityPolicy::requireCspMatchers)
3985
}
4086
}
4187
}

0 commit comments

Comments
 (0)