Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
381dc38
Fix method chaining for ContentSecurityPolicySpec
ziqin Jan 11, 2026
a91d888
Support nonce-based Content-Security-Policy
ziqin Jan 14, 2026
0ecdc80
Support configuring nonce-based Content-Security-Policy
ziqin Jan 14, 2026
1cec465
Revert "Fix method chaining for ContentSecurityPolicySpec"
ziqin Apr 1, 2026
66c0242
Make nonce-generating filters ad hoc instead of general-purpose
ziqin Mar 31, 2026
56e3884
Defer CSP nonce generation
ziqin Apr 1, 2026
6e3e094
Remove Javadoc since tags in CSP tests
ziqin Apr 1, 2026
855e946
Fix typo in ContentSecurityPolicyDsl KDoc
ziqin Apr 1, 2026
bf6f1d4
Separate internal nonce attr from attr exposed for views
ziqin Apr 1, 2026
4c838b3
Rename requireCspMatcher to requestMatcher
ziqin Apr 3, 2026
acf956a
Polish filter chain config for CSP
ziqin Apr 3, 2026
939ae10
Remove isNonceBased() from header writers
ziqin Apr 3, 2026
f666e44
Avoid tying CSP {nonce} placeholder to specific stacks
ziqin Apr 3, 2026
11279f9
Deprecate ContentSecurityPolicySpec#reportOnly(boolean)
ziqin Apr 4, 2026
32a8249
Deprecate ContentSecurityPolicySpec#policyDirectives(String)
ziqin Apr 4, 2026
0fbc676
Revert deprecation about method chaining in ContentSecurityPolicySpec
ziqin Sep 3, 2026
6c1fbf3
Move request matcher to CSP header writer and improve config
ziqin Sep 4, 2026
15a761e
Undo changes about header writer disabling and composition
ziqin Sep 4, 2026
2be35ce
Avoid failing fast in CompositeServerHttpHeadersWriter
ziqin Sep 4, 2026
cba8dd8
Avoid failing fast in CompositeHeaderWriter
ziqin Sep 4, 2026
3dae3b0
Undo changes on visibility of CSP header constants
ziqin Sep 6, 2026
aeeafb9
Introduce a getter for CSP nonce
ziqin Sep 6, 2026
9ebbf70
Update Javadoc @since version for nonce-based CSP to 7.2
ziqin Sep 6, 2026
55d55a9
Support nonce-based CSP in XML namespace config
ziqin Sep 10, 2026
d0733be
Remove isNonceBased field from ContentSecurityPolicyHeaderWriter
ziqin Sep 10, 2026
8298ded
Remove isNonceBased from ContentSecurityPolicyServerHttpHeadersWriter
ziqin Sep 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,19 @@

import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import jakarta.servlet.http.HttpServletRequest;
import org.jspecify.annotations.Nullable;

import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.configuration.ObjectPostProcessorConfiguration;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.header.ContentSecurityPolicyNonceGeneratingFilter;
import org.springframework.security.web.header.HeaderWriter;
import org.springframework.security.web.header.HeaderWriterFilter;
import org.springframework.security.web.header.writers.CacheControlHeadersWriter;
Expand All @@ -45,6 +48,8 @@
import org.springframework.security.web.header.writers.XXssProtectionHeaderWriter;
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter;
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter.XFrameOptionsMode;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;

Expand Down Expand Up @@ -75,6 +80,7 @@
* @author Vedran Pavic
* @author Ankur Pathak
* @author Daniel Garnier-Moiroux
* @author Ziqin Wang
* @since 3.2
*/
public class HeadersConfigurer<H extends HttpSecurityBuilder<H>>
Expand Down Expand Up @@ -246,6 +252,7 @@ public HeadersConfigurer<H> httpPublicKeyPinning(Customizer<HpkpConfig> hpkpCust
public HeadersConfigurer<H> contentSecurityPolicy(
Customizer<ContentSecurityPolicyConfig> contentSecurityCustomizer) {
this.contentSecurityPolicy.writer = new ContentSecurityPolicyHeaderWriter();
this.contentSecurityPolicy.nonceGeneratingFilter = new ContentSecurityPolicyNonceGeneratingFilter();
contentSecurityCustomizer.customize(this.contentSecurityPolicy);
return HeadersConfigurer.this;
}
Expand Down Expand Up @@ -273,6 +280,10 @@ public HeadersConfigurer<H> defaultsDisabled() {
public void configure(H http) {
HeaderWriterFilter headersFilter = createHeaderWriterFilter();
http.addFilter(headersFilter);
// nonceGeneratingFilter is instantiated iff CSP is configured
if (this.contentSecurityPolicy.nonceGeneratingFilter != null) {
http.addFilterBefore(this.contentSecurityPolicy.nonceGeneratingFilter, HeaderWriterFilter.class);
}
}

/**
Expand Down Expand Up @@ -937,11 +948,17 @@ public final class ContentSecurityPolicyConfig {

private ContentSecurityPolicyHeaderWriter writer;

private ContentSecurityPolicyNonceGeneratingFilter nonceGeneratingFilter;

private @Nullable RequestMatcher requestMatcher;

private ContentSecurityPolicyConfig() {
}

/**
* Sets the security policy directive(s) to be used in the response header.
* Sets the security policy directive(s) to be used in the response header. The
* {@code policyDirectives} may contain {@code {nonce}} as placeholders for a
* generated secure random nonce, e.g., {@code script-src 'self' 'nonce-{nonce}'}.
* @param policyDirectives the security policy directive(s)
* @return the {@link ContentSecurityPolicyConfig} for additional configuration
* @throws IllegalArgumentException if policyDirectives is null or empty
Expand All @@ -961,6 +978,61 @@ public ContentSecurityPolicyConfig reportOnly() {
return this;
}

/**
* Sets the name of the servlet request attribute for the generated nonce. Views
* can read this attribute to render the nonce in HTML.
* @param nonceAttributeName the name of the nonce attribute
* @return the {@link ContentSecurityPolicyConfig} for additional configuration
* @throws IllegalArgumentException if {@code nonceAttributeName} is {@code null}
* or empty
* @since 7.2
*/
public ContentSecurityPolicyConfig nonceAttributeName(String nonceAttributeName) {
Assert.hasLength(nonceAttributeName, "NonceAttributeName must not be null or empty");
this.nonceGeneratingFilter.setAttributeName(nonceAttributeName);
return this;
}

/**
* Specifies the {@link RequestMatcher} to use for determining when CSP should be
* applied. The default is to enable CSP in every response if
* {@link HeadersConfigurer#contentSecurityPolicy(Customizer)} is configured.
* @param requestMatcher the {@link RequestMatcher} to use
* @return the {@link ContentSecurityPolicyConfig} for additional configuration
* @throws IllegalArgumentException if {@code requestMatcher} is null
* @throws IllegalStateException if a {@link RequestMatcher} is already configured
* by a previous call of this method or {@link #requestMatchers(String...)}
* @since 7.2
* @see #requestMatchers(String...)
*/
public ContentSecurityPolicyConfig requestMatcher(RequestMatcher requestMatcher) {
Assert.notNull(requestMatcher, "RequestMatcher cannot be null");
Assert.state(this.requestMatcher == null, "RequestMatcher(s) is already configured");
this.writer.setRequestMatcher(requestMatcher);
this.requestMatcher = requestMatcher;
return this;
}

/**
* Specifies the matching path patterns for determining when CSP should be
* applied. The default is to enable CSP in every response if
* {@link HeadersConfigurer#contentSecurityPolicy(Customizer)} is configured.
* @param pathPatterns the path patterns to be matched with a
* {@link PathPatternRequestMatcher}
* @return the {@link ContentSecurityPolicyConfig} for additional configuration
* @throws IllegalArgumentException if any path pattern if rejected by
* {@link PathPatternRequestMatcher.Builder#matcher(String)}
* @throws IllegalStateException if a {@link RequestMatcher} is already configured
* by a previous call of this method or {@link #requestMatcher(RequestMatcher)}
* @since 7.2
* @see #requestMatcher(RequestMatcher)
*/
public ContentSecurityPolicyConfig requestMatchers(String... pathPatterns) {
PathPatternRequestMatcher.Builder builder = HeadersConfigurer.this.getRequestMatcherBuilder();
OrRequestMatcher matcher = new OrRequestMatcher(Arrays.stream(pathPatterns).map(builder::matcher).toList());
return this.requestMatcher(matcher);
}

}

public final class ReferrerPolicyConfig {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import org.springframework.beans.factory.support.ManagedList;
import org.springframework.beans.factory.xml.BeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.security.web.header.ContentSecurityPolicyNonceGeneratingFilter;
import org.springframework.security.web.header.HeaderWriterFilter;
import org.springframework.security.web.header.writers.CacheControlHeadersWriter;
import org.springframework.security.web.header.writers.ContentSecurityPolicyHeaderWriter;
Expand All @@ -54,6 +55,7 @@
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter;
import org.springframework.util.StringUtils;
import org.springframework.util.xml.DomUtils;
import org.springframework.web.filter.CompositeFilter;

/**
* Parser for the {@code HeadersFilter}.
Expand Down Expand Up @@ -133,6 +135,8 @@ public class HeadersBeanDefinitionParser implements BeanDefinitionParser {

private ManagedList<BeanMetadataElement> headerWriters;

private BeanDefinition nonceGeneratingFilter;

@Override
public BeanDefinition parse(Element element, ParserContext parserContext) {
this.headerWriters = new ManagedList<>();
Expand Down Expand Up @@ -164,7 +168,13 @@ else if (noWriters) {
return null;
}
builder.addConstructorArgValue(this.headerWriters);
return builder.getBeanDefinition();
BeanDefinition headerWriterFilter = builder.getBeanDefinition();
if (this.nonceGeneratingFilter == null) {
return headerWriterFilter;
}
return BeanDefinitionBuilder.rootBeanDefinition(CompositeFilter.class)
.addPropertyValue("filters", ManagedList.of(this.nonceGeneratingFilter, headerWriterFilter))
.getBeanDefinition();
}

/**
Expand Down Expand Up @@ -323,6 +333,9 @@ private void addContentSecurityPolicy(Element contentSecurityPolicyElement, Pars
headersWriter.addPropertyValue("reportOnly", reportOnly);
}
this.headerWriters.add(headersWriter.getBeanDefinition());
this.nonceGeneratingFilter = BeanDefinitionBuilder
.rootBeanDefinition(ContentSecurityPolicyNonceGeneratingFilter.class)
.getBeanDefinition();
}

private void parseReferrerPolicyElement(Element element, ParserContext context) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import reactor.core.publisher.Mono;
import reactor.util.context.Context;

Expand Down Expand Up @@ -183,6 +184,7 @@
import org.springframework.security.web.server.csrf.WebSessionServerCsrfTokenRepository;
import org.springframework.security.web.server.header.CacheControlServerHttpHeadersWriter;
import org.springframework.security.web.server.header.CompositeServerHttpHeadersWriter;
import org.springframework.security.web.server.header.ContentSecurityPolicyNonceGeneratingWebFilter;
import org.springframework.security.web.server.header.ContentSecurityPolicyServerHttpHeadersWriter;
import org.springframework.security.web.server.header.ContentTypeOptionsServerHttpHeadersWriter;
import org.springframework.security.web.server.header.CrossOriginEmbedderPolicyServerHttpHeadersWriter;
Expand Down Expand Up @@ -2452,6 +2454,7 @@ protected void configure(ServerHttpSecurity http) {
* Configures HTTP Response Headers.
*
* @author Rob Winch
* @author Ziqin Wang
* @since 5.0
* @see #headers(Customizer)
*/
Expand Down Expand Up @@ -2483,6 +2486,8 @@ public final class HeaderSpec {

private CrossOriginResourcePolicyServerHttpHeadersWriter crossOriginResourcePolicy = new CrossOriginResourcePolicyServerHttpHeadersWriter();

private ContentSecurityPolicyNonceGeneratingWebFilter nonceGeneratingFilter;

private HeaderSpec() {
this.writers = new ArrayList<>(Arrays.asList(this.cacheControl, this.contentTypeOptions, this.hsts,
Comment thread
ziqin marked this conversation as resolved.
this.frameOptions, this.xss, this.featurePolicy, this.permissionsPolicy, this.contentSecurityPolicy,
Expand Down Expand Up @@ -2560,6 +2565,10 @@ protected void configure(ServerHttpSecurity http) {
ServerHttpHeadersWriter writer = new CompositeServerHttpHeadersWriter(this.writers);
HttpHeaderWriterWebFilter result = new HttpHeaderWriterWebFilter(writer);
http.addFilterAt(result, SecurityWebFiltersOrder.HTTP_HEADERS_WRITER);
// nonceGeneratingFilter is instantiated iff CSP is configured
if (this.nonceGeneratingFilter != null) {
http.addFilterBefore(this.nonceGeneratingFilter, SecurityWebFiltersOrder.HTTP_HEADERS_WRITER);
}
}

/**
Expand Down Expand Up @@ -2836,8 +2845,11 @@ public final class ContentSecurityPolicySpec {

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

private @Nullable ServerWebExchangeMatcher exchangeMatcher;

private ContentSecurityPolicySpec() {
HeaderSpec.this.contentSecurityPolicy.setPolicyDirectives(DEFAULT_SRC_SELF_POLICY);
HeaderSpec.this.nonceGeneratingFilter = new ContentSecurityPolicyNonceGeneratingWebFilter();
}

/**
Expand All @@ -2854,6 +2866,9 @@ public HeaderSpec reportOnly(boolean reportOnly) {

/**
* Sets the security policy directive(s) to be used in the response header.
* The {@code policyDirectives} may contain {@code {nonce}} as placeholders
* for a generated secure random nonce, e.g., {@code script-src 'self'
* 'nonce-{nonce}'}.
* @param policyDirectives the security policy directive(s)
* @return the {@link HeaderSpec} to continue configuring
*/
Expand All @@ -2862,8 +2877,60 @@ public HeaderSpec policyDirectives(String policyDirectives) {
return HeaderSpec.this;
}

private ContentSecurityPolicySpec(String policyDirectives) {
HeaderSpec.this.contentSecurityPolicy.setPolicyDirectives(policyDirectives);
/**
* Sets the name of the {@link ServerWebExchange#getAttribute(String) exchange
* attribute} for the generated nonce. Views can read this attribute to render
* the nonce in HTML.
* @param nonceAttributeName the name of the nonce attribute
* @return the {@link ContentSecurityPolicySpec} to continue configuring
* @throws IllegalArgumentException if {@code nonceAttributeName} is
* {@code null} or empty
* @since 7.2
*/
public ContentSecurityPolicySpec nonceAttributeName(String nonceAttributeName) {
Assert.hasLength(nonceAttributeName, "NonceAttributeName must not be null or empty");
HeaderSpec.this.nonceGeneratingFilter.setAttributeName(nonceAttributeName);
return this;
}

/**
* Specifies the {@link ServerWebExchangeMatcher} to use for determining when
* CSP should be applied. The default is to enable CSP in every response if
* {@link HeaderSpec#contentSecurityPolicy(Customizer)} is configured.
* @param matcher the {@link ServerWebExchangeMatcher} to use
* @return the {@link ContentSecurityPolicySpec} to continue configuring
* @throws IllegalArgumentException if {@code matcher} is {@code null}
* @throws IllegalStateException if a {@link ServerWebExchangeMatcher} is
* already configured by a previous call of this method or
* {@link #exchangeMatchers(String...)}
* @since 7.2
* @see #exchangeMatchers(String...)
*/
public ContentSecurityPolicySpec exchangeMatcher(ServerWebExchangeMatcher matcher) {
Assert.notNull(matcher, "Matcher must not be null");
Assert.state(this.exchangeMatcher == null, "ExchangeMatcher(s) is already configured");
HeaderSpec.this.contentSecurityPolicy.setExchangeMatcher(matcher);
this.exchangeMatcher = matcher;
return this;
}

/**
* Specifies the matching path patterns for determining when CSP should be
* applied. The default is to enable CSP in every response if
* {@link HeaderSpec#contentSecurityPolicy(Customizer)} is configured.
* @param pathPatterns the path patterns to be matched with a
* {@link PathPatternParserServerWebExchangeMatcher}
* @return the {@link ContentSecurityPolicySpec} to continue configuring
* @throws IllegalArgumentException if any path pattern is rejected by
* {@link PathPatternParserServerWebExchangeMatcher}
* @throws IllegalStateException if a {@link ServerWebExchangeMatcher} is
* already configured by a previous call of this method or
* {@link #exchangeMatcher(ServerWebExchangeMatcher)}
* @since 7.2
* @see #exchangeMatcher(ServerWebExchangeMatcher)
*/
public ContentSecurityPolicySpec exchangeMatchers(String... pathPatterns) {
return this.exchangeMatcher(ServerWebExchangeMatchers.pathMatchers(pathPatterns));
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,63 @@ package org.springframework.security.config.annotation.web.headers

import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer
import org.springframework.security.web.util.matcher.RequestMatcher

/**
* A Kotlin DSL to configure the [HttpSecurity] Content-Security-Policy header using
* idiomatic Kotlin code.
*
* @author Eleftheria Stein
* @author Ziqin Wang
* @since 5.3
* @property policyDirectives the security policy directive(s) to be used in the response header.
* @property reportOnly includes the Content-Security-Policy-Report-Only header in the response.
*/
@HeadersSecurityMarker
class ContentSecurityPolicyDsl {
/**
* The security policy directive(s) to be used in the response header.
* The [policyDirectives] may contain `{nonce}` as placeholders for a generated secure
* random nonce, e.g., `script-src 'self' 'nonce-{nonce}'`.
*/
var policyDirectives: String? = null

/** Includes the `Content-Security-Policy-Report-Only` header in the response. */
var reportOnly: Boolean? = null

/**
* The name of the servlet request attribute for the generated nonce. Views can read
* this attribute to render the nonce in HTML.
* @since 7.2
*/
var nonceAttributeName: String? = null

/**
* The [RequestMatcher] to use for determining when CSP should be applied.
* The default is to enable CSP in every response if
* [org.springframework.security.config.annotation.web.HeadersDsl.contentSecurityPolicy]
* is configured.
* You can configure either this property or [requestMatchers], but not both.
* @since 7.2
* @see requestMatchers
*/
var requestMatcher: RequestMatcher? = null

private var requireCspPathPatterns: Array<out String>? = null

/**
* Specify the matching path patterns for determining when CSP should be applied.
* The default is to write CSP header in every response if
* [org.springframework.security.config.annotation.web.HeadersDsl.contentSecurityPolicy]
* is configured.
* You can configure either this method or [requestMatcher], but not both.
* @param pathPatterns the path patterns to be matched with a
* [org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher]
* @since 7.2
* @see requestMatcher
*/
fun requestMatchers(vararg pathPatterns: String) {
requireCspPathPatterns = pathPatterns
}

internal fun get(): (HeadersConfigurer<HttpSecurity>.ContentSecurityPolicyConfig) -> Unit {
return { contentSecurityPolicy ->
policyDirectives?.also {
Expand All @@ -43,6 +85,9 @@ class ContentSecurityPolicyDsl {
contentSecurityPolicy.reportOnly()
}
}
nonceAttributeName?.also(contentSecurityPolicy::nonceAttributeName)
requestMatcher?.also(contentSecurityPolicy::requestMatcher)
requireCspPathPatterns?.also(contentSecurityPolicy::requestMatchers)
}
}
}
Loading
Loading