Skip to content

Commit acf956a

Browse files
committed
Polish filter chain config for CSP
Signed-off-by: Ziqin Wang <ziqin@wangziqin.net>
1 parent 4c838b3 commit acf956a

2 files changed

Lines changed: 56 additions & 42 deletions

File tree

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

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -280,18 +280,7 @@ public HeadersConfigurer<H> defaultsDisabled() {
280280
public void configure(H http) {
281281
HeaderWriterFilter headersFilter = createHeaderWriterFilter();
282282
http.addFilter(headersFilter);
283-
configureCspNonceGeneratingFilter(http);
284-
}
285-
286-
private void configureCspNonceGeneratingFilter(H http) {
287-
ContentSecurityPolicyHeaderWriter writer = this.contentSecurityPolicy.writer;
288-
if (writer != null && writer.isNonceBased()) {
289-
ContentSecurityPolicyNonceGeneratingFilter filter = new ContentSecurityPolicyNonceGeneratingFilter();
290-
if (this.contentSecurityPolicy.nonceAttributeName != null) {
291-
filter.setAttributeName(this.contentSecurityPolicy.nonceAttributeName);
292-
}
293-
http.addFilterBefore(filter, HeaderWriterFilter.class);
294-
}
283+
http.addFilterBefore(this.contentSecurityPolicy.getNonceGeneratingFilter(), HeaderWriterFilter.class);
295284
}
296285

297286
/**
@@ -1047,6 +1036,14 @@ HeaderWriter getWriter() {
10471036
return this.writer;
10481037
}
10491038

1039+
ContentSecurityPolicyNonceGeneratingFilter getNonceGeneratingFilter() {
1040+
var filter = new ContentSecurityPolicyNonceGeneratingFilter();
1041+
if (this.nonceAttributeName != null) {
1042+
filter.setAttributeName(this.nonceAttributeName);
1043+
}
1044+
return filter;
1045+
}
1046+
10501047
}
10511048

10521049
public final class ReferrerPolicyConfig {

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

Lines changed: 47 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,11 @@
3434
import java.util.function.Consumer;
3535
import java.util.function.Function;
3636
import java.util.function.Supplier;
37+
import java.util.stream.Stream;
3738

3839
import org.apache.commons.logging.Log;
3940
import org.apache.commons.logging.LogFactory;
41+
import org.jspecify.annotations.Nullable;
4042
import reactor.core.publisher.Mono;
4143
import reactor.util.context.Context;
4244

@@ -2460,8 +2462,6 @@ protected void configure(ServerHttpSecurity http) {
24602462
*/
24612463
public final class HeaderSpec {
24622464

2463-
private final List<ServerHttpHeadersWriter> writers;
2464-
24652465
private CacheControlServerHttpHeadersWriter cacheControl = new CacheControlServerHttpHeadersWriter();
24662466

24672467
private ContentTypeOptionsServerHttpHeadersWriter contentTypeOptions = new ContentTypeOptionsServerHttpHeadersWriter();
@@ -2476,7 +2476,7 @@ public final class HeaderSpec {
24762476

24772477
private PermissionsPolicyServerHttpHeadersWriter permissionsPolicy = new PermissionsPolicyServerHttpHeadersWriter();
24782478

2479-
private ContentSecurityPolicyServerHttpHeadersWriter contentSecurityPolicy = new ContentSecurityPolicyServerHttpHeadersWriter();
2479+
private ContentSecurityPolicySpec contentSecurityPolicy = new ContentSecurityPolicySpec();
24802480

24812481
private ReferrerPolicyServerHttpHeadersWriter referrerPolicy = new ReferrerPolicyServerHttpHeadersWriter();
24822482

@@ -2486,13 +2486,9 @@ public final class HeaderSpec {
24862486

24872487
private CrossOriginResourcePolicyServerHttpHeadersWriter crossOriginResourcePolicy = new CrossOriginResourcePolicyServerHttpHeadersWriter();
24882488

2489-
private final ContentSecurityPolicyNonceGeneratingWebFilter nonceGeneratingFilter = new ContentSecurityPolicyNonceGeneratingWebFilter();
2489+
private List<ServerHttpHeadersWriter> customHeadersWriters = new ArrayList<>();
24902490

24912491
private HeaderSpec() {
2492-
this.writers = new ArrayList<>(Arrays.asList(this.cacheControl, this.contentTypeOptions, this.hsts,
2493-
this.frameOptions, this.xss, this.featurePolicy, this.permissionsPolicy, this.contentSecurityPolicy,
2494-
this.referrerPolicy, this.crossOriginOpenerPolicy, this.crossOriginEmbedderPolicy,
2495-
this.crossOriginResourcePolicy));
24962492
}
24972493

24982494
/**
@@ -2546,7 +2542,7 @@ public HeaderSpec frameOptions(Customizer<FrameOptionsSpec> frameOptionsCustomiz
25462542
*/
25472543
public HeaderSpec writer(ServerHttpHeadersWriter serverHttpHeadersWriter) {
25482544
Assert.notNull(serverHttpHeadersWriter, "serverHttpHeadersWriter cannot be null");
2549-
this.writers.add(serverHttpHeadersWriter);
2545+
this.customHeadersWriters.add(serverHttpHeadersWriter);
25502546
return this;
25512547
}
25522548

@@ -2562,12 +2558,18 @@ public HeaderSpec hsts(Customizer<HstsSpec> hstsCustomizer) {
25622558
}
25632559

25642560
protected void configure(ServerHttpSecurity http) {
2565-
ServerHttpHeadersWriter writer = new CompositeServerHttpHeadersWriter(this.writers);
2561+
Stream<ServerHttpHeadersWriter> builtInWriters = Stream
2562+
.of(this.cacheControl, this.contentTypeOptions, this.hsts, this.frameOptions, this.xss,
2563+
this.featurePolicy, this.permissionsPolicy, this.contentSecurityPolicy.getWriter(),
2564+
this.referrerPolicy, this.crossOriginOpenerPolicy, this.crossOriginEmbedderPolicy,
2565+
this.crossOriginResourcePolicy)
2566+
.filter(Objects::nonNull);
2567+
ServerHttpHeadersWriter writer = new CompositeServerHttpHeadersWriter(
2568+
Stream.concat(builtInWriters, this.customHeadersWriters.stream()).toList());
25662569
HttpHeaderWriterWebFilter result = new HttpHeaderWriterWebFilter(writer);
25672570
http.addFilterAt(result, SecurityWebFiltersOrder.HTTP_HEADERS_WRITER);
2568-
if (this.contentSecurityPolicy.isNonceBased()) {
2569-
http.addFilterBefore(this.nonceGeneratingFilter, SecurityWebFiltersOrder.HTTP_HEADERS_WRITER);
2570-
}
2571+
http.addFilterBefore(this.contentSecurityPolicy.getNonceGeneratingFilter(),
2572+
SecurityWebFiltersOrder.HTTP_HEADERS_WRITER);
25712573
}
25722574

25732575
/**
@@ -2588,7 +2590,7 @@ public HeaderSpec xssProtection(Customizer<XssProtectionSpec> xssProtectionCusto
25882590
* @return the {@link HeaderSpec} to customize
25892591
*/
25902592
public HeaderSpec contentSecurityPolicy(Customizer<ContentSecurityPolicySpec> contentSecurityPolicyCustomizer) {
2591-
contentSecurityPolicyCustomizer.customize(new ContentSecurityPolicySpec());
2593+
contentSecurityPolicyCustomizer.customize(this.contentSecurityPolicy);
25922594
return this;
25932595
}
25942596

@@ -2683,7 +2685,7 @@ private CacheSpec() {
26832685
* @return the {@link HeaderSpec} to configure
26842686
*/
26852687
public HeaderSpec disable() {
2686-
HeaderSpec.this.writers.remove(HeaderSpec.this.cacheControl);
2688+
HeaderSpec.this.cacheControl = null;
26872689
return HeaderSpec.this;
26882690
}
26892691

@@ -2704,7 +2706,7 @@ private ContentTypeOptionsSpec() {
27042706
* @return the {@link HeaderSpec} to configure
27052707
*/
27062708
public HeaderSpec disable() {
2707-
HeaderSpec.this.writers.remove(HeaderSpec.this.contentTypeOptions);
2709+
HeaderSpec.this.contentTypeOptions = null;
27082710
return HeaderSpec.this;
27092711
}
27102712

@@ -2736,7 +2738,7 @@ public HeaderSpec mode(XFrameOptionsServerHttpHeadersWriter.Mode mode) {
27362738
* @return the {@link HeaderSpec} to continue configuring
27372739
*/
27382740
public HeaderSpec disable() {
2739-
HeaderSpec.this.writers.remove(HeaderSpec.this.frameOptions);
2741+
HeaderSpec.this.frameOptions = null;
27402742
return HeaderSpec.this;
27412743
}
27422744

@@ -2795,7 +2797,7 @@ public HstsSpec preload(boolean preload) {
27952797
* @return the {@link HeaderSpec} to continue configuring
27962798
*/
27972799
public HeaderSpec disable() {
2798-
HeaderSpec.this.writers.remove(HeaderSpec.this.hsts);
2800+
HeaderSpec.this.hsts = null;
27992801
return HeaderSpec.this;
28002802
}
28012803

@@ -2816,7 +2818,7 @@ private XssProtectionSpec() {
28162818
* @return the {@link HeaderSpec} to continue configuring
28172819
*/
28182820
public HeaderSpec disable() {
2819-
HeaderSpec.this.writers.remove(HeaderSpec.this.xss);
2821+
HeaderSpec.this.xss = null;
28202822
return HeaderSpec.this;
28212823
}
28222824

@@ -2844,8 +2846,14 @@ public final class ContentSecurityPolicySpec {
28442846

28452847
private static final String DEFAULT_SRC_SELF_POLICY = "default-src 'self'";
28462848

2849+
private final ContentSecurityPolicyServerHttpHeadersWriter writer = new ContentSecurityPolicyServerHttpHeadersWriter();
2850+
2851+
private @Nullable String nonceAttributeName;
2852+
2853+
private @Nullable ServerWebExchangeMatcher exchangeMatcher;
2854+
28472855
private ContentSecurityPolicySpec() {
2848-
HeaderSpec.this.contentSecurityPolicy.setPolicyDirectives(DEFAULT_SRC_SELF_POLICY);
2856+
this.writer.setPolicyDirectives(DEFAULT_SRC_SELF_POLICY);
28492857
}
28502858

28512859
/**
@@ -2856,7 +2864,7 @@ private ContentSecurityPolicySpec() {
28562864
* @return the {@link HeaderSpec} to continue configuring
28572865
*/
28582866
public HeaderSpec reportOnly(boolean reportOnly) {
2859-
HeaderSpec.this.contentSecurityPolicy.setReportOnly(reportOnly);
2867+
this.writer.setReportOnly(reportOnly);
28602868
return HeaderSpec.this;
28612869
}
28622870

@@ -2869,7 +2877,7 @@ public HeaderSpec reportOnly(boolean reportOnly) {
28692877
* @return the {@link HeaderSpec} to continue configuring
28702878
*/
28712879
public HeaderSpec policyDirectives(String policyDirectives) {
2872-
HeaderSpec.this.contentSecurityPolicy.setPolicyDirectives(policyDirectives);
2880+
this.writer.setPolicyDirectives(policyDirectives);
28732881
return HeaderSpec.this;
28742882
}
28752883

@@ -2884,7 +2892,8 @@ public HeaderSpec policyDirectives(String policyDirectives) {
28842892
* @since 7.1
28852893
*/
28862894
public ContentSecurityPolicySpec nonceAttributeName(String nonceAttributeName) {
2887-
HeaderSpec.this.nonceGeneratingFilter.setAttributeName(nonceAttributeName);
2895+
Assert.hasLength(nonceAttributeName, "NonceAttributeName must not be null or empty");
2896+
this.nonceAttributeName = nonceAttributeName;
28882897
return this;
28892898
}
28902899

@@ -2903,11 +2912,8 @@ public ContentSecurityPolicySpec nonceAttributeName(String nonceAttributeName) {
29032912
*/
29042913
public ContentSecurityPolicySpec exchangeMatcher(ServerWebExchangeMatcher matcher) {
29052914
Assert.notNull(matcher, "Matcher must not be null");
2906-
// Replace the CSP writer in the list with a matcher-decorated writer
2907-
int idx = HeaderSpec.this.writers.indexOf(HeaderSpec.this.contentSecurityPolicy);
2908-
Assert.state(idx >= 0, "ExchangeMatcher(s) is already configured");
2909-
HeaderSpec.this.writers.set(idx, new ServerWebExchangeDelegatingServerHttpHeadersWriter(matcher,
2910-
HeaderSpec.this.contentSecurityPolicy));
2915+
Assert.state(this.exchangeMatcher == null, "ExchangeMatcher(s) is already configured");
2916+
this.exchangeMatcher = matcher;
29112917
return this;
29122918
}
29132919

@@ -2930,8 +2936,19 @@ public ContentSecurityPolicySpec exchangeMatchers(String... pathPatterns) {
29302936
return this.exchangeMatcher(ServerWebExchangeMatchers.pathMatchers(pathPatterns));
29312937
}
29322938

2933-
private ContentSecurityPolicySpec(String policyDirectives) {
2934-
HeaderSpec.this.contentSecurityPolicy.setPolicyDirectives(policyDirectives);
2939+
ServerHttpHeadersWriter getWriter() {
2940+
if (this.exchangeMatcher != null) {
2941+
return new ServerWebExchangeDelegatingServerHttpHeadersWriter(this.exchangeMatcher, this.writer);
2942+
}
2943+
return this.writer;
2944+
}
2945+
2946+
ContentSecurityPolicyNonceGeneratingWebFilter getNonceGeneratingFilter() {
2947+
var filter = new ContentSecurityPolicyNonceGeneratingWebFilter();
2948+
if (this.nonceAttributeName != null) {
2949+
filter.setAttributeName(this.nonceAttributeName);
2950+
}
2951+
return filter;
29352952
}
29362953

29372954
}

0 commit comments

Comments
 (0)