Skip to content

Commit ce1d4b6

Browse files
committed
Add Reporting-Endpoints header support to ContentSecurityPolicyConfig
Reporting-Endpoints is a full member of CSP (Content Security Policy), and if we ignore deprecations, it's the third member. The others have already been added and implemented in ContentSecurityPolicyConfig and ContentSecurityPolicyHeaderWriter. The goal of this commit is to provide a simpler API for interacting with and installing Reporting-Endpoints. Closes: gh-18783 Signed-off-by: Andrey Litvitski <andrey1010102008@gmail.com>
1 parent b19e0e1 commit ce1d4b6

File tree

3 files changed

+142
-4
lines changed

3 files changed

+142
-4
lines changed

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
* @author Vedran Pavic
7676
* @author Ankur Pathak
7777
* @author Daniel Garnier-Moiroux
78+
* @author Andrey Litvitski
7879
* @since 3.2
7980
*/
8081
public class HeadersConfigurer<H extends HttpSecurityBuilder<H>>
@@ -951,6 +952,17 @@ public ContentSecurityPolicyConfig policyDirectives(String policyDirectives) {
951952
return this;
952953
}
953954

955+
/**
956+
* Sets the reporting endpoints to be used in the response header.
957+
* @param reportingEndpoints the reporting endpoints
958+
* @return the {@link ContentSecurityPolicyConfig} for additional configuration
959+
* @throws IllegalArgumentException if reportingEndpoints is null or empty
960+
*/
961+
public ContentSecurityPolicyConfig reportingEndpoints(String reportingEndpoints) {
962+
this.writer.setReportingEndpoints(reportingEndpoints);
963+
return this;
964+
}
965+
954966
/**
955967
* Enables (includes) the Content-Security-Policy-Report-Only header in the
956968
* response.

config/src/test/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerTests.java

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
* @author Eleftheria Stein
6262
* @author Marcus Da Coregio
6363
* @author Daniel Garnier-Moiroux
64+
* @author Andrey Litvitski
6465
*/
6566
@ExtendWith(SpringTestContextExtension.class)
6667
public class HeadersConfigurerTests {
@@ -575,6 +576,105 @@ public void getWhenCustomCrossOriginPoliciesThenCrossOriginPolicyHeadersWithCust
575576
HttpHeaders.CROSS_ORIGIN_EMBEDDER_POLICY, HttpHeaders.CROSS_ORIGIN_RESOURCE_POLICY);
576577
}
577578

579+
@Test
580+
public void getWhenContentSecurityPolicyWithReportingEndpointsThenBothHeadersInResponse() throws Exception {
581+
this.spring.register(ContentSecurityPolicyWithReportingEndpointsConfig.class).autowire();
582+
ResultMatcher csp = header().string(HttpHeaders.CONTENT_SECURITY_POLICY,
583+
"default-src 'self'; report-to csp-endpoint");
584+
ResultMatcher reportingEndpoints = header().string("Reporting-Endpoints",
585+
"csp-endpoint=\"https://example.com/csp-reports\"");
586+
// @formatter:off
587+
MvcResult mvcResult = this.mvc.perform(get("/").secure(true))
588+
.andExpect(csp)
589+
.andExpect(reportingEndpoints)
590+
.andReturn();
591+
// @formatter:on
592+
assertThat(mvcResult.getResponse().getHeaderNames())
593+
.containsExactlyInAnyOrder(HttpHeaders.CONTENT_SECURITY_POLICY, "Reporting-Endpoints");
594+
}
595+
596+
@Test
597+
public void getWhenContentSecurityPolicyReportOnlyWithReportingEndpointsThenBothHeadersInResponse()
598+
throws Exception {
599+
this.spring.register(ContentSecurityPolicyReportOnlyWithReportingEndpointsConfig.class).autowire();
600+
ResultMatcher csp = header().string(HttpHeaders.CONTENT_SECURITY_POLICY_REPORT_ONLY,
601+
"default-src 'self'; report-to csp-endpoint");
602+
ResultMatcher reportingEndpoints = header().string("Reporting-Endpoints",
603+
"csp-endpoint=\"https://example.com/csp-reports\"");
604+
// @formatter:off
605+
MvcResult mvcResult = this.mvc.perform(get("/").secure(true))
606+
.andExpect(csp)
607+
.andExpect(reportingEndpoints)
608+
.andReturn();
609+
// @formatter:on
610+
assertThat(mvcResult.getResponse().getHeaderNames())
611+
.containsExactlyInAnyOrder(HttpHeaders.CONTENT_SECURITY_POLICY_REPORT_ONLY, "Reporting-Endpoints");
612+
}
613+
614+
@Test
615+
public void getWhenContentSecurityPolicyWithInvalidReportingEndpointsThenBeanCreationException() {
616+
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(
617+
() -> this.spring.register(ContentSecurityPolicyWithInvalidReportingEndpointsConfig.class).autowire());
618+
}
619+
620+
@Configuration
621+
@EnableWebSecurity
622+
static class ContentSecurityPolicyWithReportingEndpointsConfig {
623+
624+
@Bean
625+
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
626+
// @formatter:off
627+
http
628+
.headers((headers) -> headers
629+
.defaultsDisabled()
630+
.contentSecurityPolicy((csp) -> csp
631+
.policyDirectives("default-src 'self'; report-to csp-endpoint")
632+
.reportingEndpoints("csp-endpoint=\"https://example.com/csp-reports\"")));
633+
return http.build();
634+
// @formatter:on
635+
}
636+
637+
}
638+
639+
@Configuration
640+
@EnableWebSecurity
641+
static class ContentSecurityPolicyReportOnlyWithReportingEndpointsConfig {
642+
643+
@Bean
644+
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
645+
// @formatter:off
646+
http
647+
.headers((headers) -> headers
648+
.defaultsDisabled()
649+
.contentSecurityPolicy((csp) -> csp
650+
.policyDirectives("default-src 'self'; report-to csp-endpoint")
651+
.reportOnly()
652+
.reportingEndpoints("csp-endpoint=\"https://example.com/csp-reports\"")));
653+
return http.build();
654+
// @formatter:on
655+
}
656+
657+
}
658+
659+
@Configuration
660+
@EnableWebSecurity
661+
static class ContentSecurityPolicyWithInvalidReportingEndpointsConfig {
662+
663+
@Bean
664+
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
665+
// @formatter:off
666+
http
667+
.headers((headers) -> headers
668+
.defaultsDisabled()
669+
.contentSecurityPolicy((csp) -> csp
670+
.policyDirectives("default-src 'self'")
671+
.reportingEndpoints("")));
672+
return http.build();
673+
// @formatter:on
674+
}
675+
676+
}
677+
578678
@Configuration
579679
@EnableWebSecurity
580680
static class HeadersConfig {

web/src/main/java/org/springframework/security/web/header/writers/ContentSecurityPolicyHeaderWriter.java

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import jakarta.servlet.http.HttpServletRequest;
2020
import jakarta.servlet.http.HttpServletResponse;
2121

22+
import org.jspecify.annotations.Nullable;
2223
import org.springframework.security.web.header.HeaderWriter;
2324
import org.springframework.util.Assert;
2425

@@ -60,6 +61,7 @@
6061
* <ul>
6162
* <li>Content-Security-Policy</li>
6263
* <li>Content-Security-Policy-Report-Only</li>
64+
* <li>Reporting-Endpoints (optional)</li>
6365
* </ul>
6466
*
6567
* <p>
@@ -71,6 +73,12 @@
7173
* </p>
7274
*
7375
* <p>
76+
* To enable violation reporting, use {@link #setReportingEndpoints(String)} to declare
77+
* one or more reporting endpoints, and include the {@code report-to} directive in the
78+
* policy.
79+
* </p>
80+
*
81+
* <p>
7482
* <strong> CSP is not intended as a first line of defense against content injection
7583
* vulnerabilities. Instead, CSP is used to reduce the harm caused by content injection
7684
* attacks. As a first line of defense against content injection, web application authors
@@ -79,6 +87,7 @@
7987
*
8088
* @author Joe Grandja
8189
* @author Ankur Pathak
90+
* @author Andrey Litvitski
8291
* @since 4.1
8392
*/
8493
public final class ContentSecurityPolicyHeaderWriter implements HeaderWriter {
@@ -87,12 +96,16 @@ public final class ContentSecurityPolicyHeaderWriter implements HeaderWriter {
8796

8897
private static final String CONTENT_SECURITY_POLICY_REPORT_ONLY_HEADER = "Content-Security-Policy-Report-Only";
8998

99+
private static final String REPORTING_ENDPOINTS_HEADER = "Reporting-Endpoints";
100+
90101
private static final String DEFAULT_SRC_SELF_POLICY = "default-src 'self'";
91102

92103
private String policyDirectives;
93104

94105
private boolean reportOnly;
95106

107+
@Nullable private String reportingEndpoints;
108+
96109
/**
97110
* Creates a new instance. Default value: default-src 'self'
98111
*/
@@ -117,10 +130,13 @@ public ContentSecurityPolicyHeaderWriter(String policyDirectives) {
117130
*/
118131
@Override
119132
public void writeHeaders(HttpServletRequest request, HttpServletResponse response) {
120-
String headerName = (!this.reportOnly) ? CONTENT_SECURITY_POLICY_HEADER
133+
String policyHeader = (!this.reportOnly) ? CONTENT_SECURITY_POLICY_HEADER
121134
: CONTENT_SECURITY_POLICY_REPORT_ONLY_HEADER;
122-
if (!response.containsHeader(headerName)) {
123-
response.setHeader(headerName, this.policyDirectives);
135+
if (!response.containsHeader(policyHeader)) {
136+
response.setHeader(policyHeader, this.policyDirectives);
137+
}
138+
if (this.reportingEndpoints != null && !response.containsHeader(REPORTING_ENDPOINTS_HEADER)) {
139+
response.setHeader(REPORTING_ENDPOINTS_HEADER, this.reportingEndpoints);
124140
}
125141
}
126142

@@ -134,6 +150,16 @@ public void setPolicyDirectives(String policyDirectives) {
134150
this.policyDirectives = policyDirectives;
135151
}
136152

153+
/**
154+
* Sets the reporting endpoints to be used in the response header.
155+
* @param reportingEndpoints the reporting endpoints
156+
* @throws IllegalArgumentException if reportingEndpoints is null or empty
157+
*/
158+
public void setReportingEndpoints(String reportingEndpoints) {
159+
Assert.hasLength(reportingEndpoints, "reportingEndpoints cannot be null or empty");
160+
this.reportingEndpoints = reportingEndpoints;
161+
}
162+
137163
/**
138164
* If true, includes the Content-Security-Policy-Report-Only header in the response,
139165
* otherwise, defaults to the Content-Security-Policy header.
@@ -146,7 +172,7 @@ public void setReportOnly(boolean reportOnly) {
146172
@Override
147173
public String toString() {
148174
return getClass().getName() + " [policyDirectives=" + this.policyDirectives + "; reportOnly=" + this.reportOnly
149-
+ "]";
175+
+ "; reportingEndpoints= " + this.reportingEndpoints + "]";
150176
}
151177

152178
}

0 commit comments

Comments
 (0)