Skip to content

Commit 9faf064

Browse files
committed
fix(security): set secure response headers in any condition of SdaSecurityConfiguration
1 parent 66f54ca commit 9faf064

File tree

11 files changed

+281
-264
lines changed

11 files changed

+281
-264
lines changed

sda-commons-web-autoconfigure/src/main/java/org/sdase/commons/spring/boot/web/auth/SdaSecurityConfiguration.java

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
package org.sdase.commons.spring.boot.web.auth;
99

1010
import java.util.List;
11+
import java.util.Optional;
1112
import java.util.stream.Stream;
1213
import javax.servlet.http.HttpServletRequest;
14+
import org.sdase.commons.spring.boot.web.security.headers.SdaSecurityHeaders;
1315
import org.slf4j.Logger;
1416
import org.slf4j.LoggerFactory;
1517
import org.springframework.beans.factory.annotation.Value;
@@ -26,6 +28,7 @@
2628
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
2729
import org.springframework.security.oauth2.server.resource.authentication.JwtIssuerAuthenticationManagerResolver;
2830
import org.springframework.security.web.SecurityFilterChain;
31+
import org.springframework.security.web.header.writers.StaticHeadersWriter;
2932
import org.springframework.util.StringUtils;
3033

3134
@EnableWebSecurity
@@ -41,6 +44,8 @@ public class SdaSecurityConfiguration {
4144

4245
private final SdaAccessDecisionManager sdaAccessDecisionManager;
4346

47+
private final SdaSecurityHeaders sdaSecurityHeaders;
48+
4449
/**
4550
* @param issuers Comma separated string of open id discovery key sources with required issuers.
4651
* @param disableAuthentication Disables all authentication
@@ -50,10 +55,12 @@ public class SdaSecurityConfiguration {
5055
public SdaSecurityConfiguration(
5156
@Value("${auth.issuers:}") String issuers,
5257
@Value("${auth.disable:false}") boolean disableAuthentication,
53-
SdaAccessDecisionManager sdaAccessDecisionManager) {
58+
SdaAccessDecisionManager sdaAccessDecisionManager,
59+
Optional<SdaSecurityHeaders> sdaSecurityHeaders) {
5460
this.issuers = issuers;
5561
this.disableAuthentication = disableAuthentication;
5662
this.sdaAccessDecisionManager = sdaAccessDecisionManager;
63+
this.sdaSecurityHeaders = sdaSecurityHeaders.orElse(List::of);
5764
}
5865

5966
@Bean
@@ -82,7 +89,11 @@ private void oidcAuthentication(HttpSecurity http) throws Exception {
8289
authorize ->
8390
authorize.anyRequest().permitAll().accessDecisionManager(sdaAccessDecisionManager))
8491
.oauth2ResourceServer(
85-
oauth2 -> oauth2.authenticationManagerResolver(authenticationManagerResolver));
92+
oauth2 -> oauth2.authenticationManagerResolver(authenticationManagerResolver))
93+
.headers(
94+
configurer ->
95+
configurer.addHeaderWriter(
96+
new StaticHeadersWriter(sdaSecurityHeaders.getSecurityHeaders())));
8697
}
8798

8899
private AuthenticationManagerResolver<HttpServletRequest> createAuthenticationManagerResolver() {
@@ -111,7 +122,11 @@ private void noAuthentication(HttpSecurity http) throws Exception {
111122
.disable() // NOSONAR
112123
.authorizeRequests(
113124
authorize ->
114-
authorize.anyRequest().permitAll().accessDecisionManager(sdaAccessDecisionManager));
125+
authorize.anyRequest().permitAll().accessDecisionManager(sdaAccessDecisionManager))
126+
.headers(
127+
configurer ->
128+
configurer.addHeaderWriter(
129+
new StaticHeadersWriter(sdaSecurityHeaders.getSecurityHeaders())));
115130
}
116131

117132
private List<String> commaSeparatedStringToList(String issuers) {

sda-commons-web-autoconfigure/src/main/java/org/sdase/commons/spring/boot/web/security/headers/EnableFrontendSecurity.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,24 @@
1313
import java.lang.annotation.Target;
1414
import org.springframework.context.annotation.Import;
1515

16+
/**
17+
* Activates setting headers to the response that enhance the security of web applications. Usually
18+
* we do not provide web content from services. But we address the risks identified in the security
19+
* guide as:
20+
*
21+
* <ul>
22+
* <li>"Risk: Cross Site Scripting (XSS)"
23+
* <li>"Risk: Reloading content into Flash and PDFs"
24+
* <li>"Risk: Clickjacking"
25+
* <li>"Risk: Passing on visited URLs to third parties"
26+
* <li>"Risk: Interpretation of content by the browser"
27+
* </ul>
28+
*
29+
* <p>This feature should only be enabled in services that provide frontend resources like HTML
30+
* pages themselves. Services that provide REST APIs only shall not activate this feature. If not
31+
* enabled, headers are set {@linkplain RestfulApiSecurityConfiguration according risks for backend
32+
* service}.
33+
*/
1634
@Retention(RetentionPolicy.RUNTIME)
1735
@Target(ElementType.TYPE)
1836
@Import({FrontendSecurityConfiguration.class})

sda-commons-web-autoconfigure/src/main/java/org/sdase/commons/spring/boot/web/security/headers/FrontendSecurityConfiguration.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@
88
package org.sdase.commons.spring.boot.web.security.headers;
99

1010
import org.springframework.context.annotation.Bean;
11-
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
12-
import org.springframework.security.web.SecurityFilterChain;
1311

1412
/**
1513
* This filter adds headers to the response that enhance the security of web applications. Usually
@@ -23,15 +21,18 @@
2321
* <li>"Risk: Passing on visited URLs to third parties"
2422
* <li>"Risk: Interpretation of content by the browser"
2523
* </ul>
24+
*
25+
* <p>This feature should only be enabled in services that provide frontend resources like HTML
26+
* pages themselves. Services that provide REST APIs only shall not activate this feature. If not
27+
* enabled, headers are set {@linkplain RestfulApiSecurityConfiguration according risks for backend
28+
* service}.
2629
*/
2730
public class FrontendSecurityConfiguration {
2831

2932
public static final String FRONTEND_SECURITY_ADVICE_BEAN_NAME = "frontendHeadersAdvice";
3033

3134
@Bean(FRONTEND_SECURITY_ADVICE_BEAN_NAME)
32-
public SecurityFilterChain frontendHeadersAdvice(HttpSecurity http) throws Exception {
33-
// add specific headers for frontends
34-
http.headers(HttpHeadersAdvice::addFrontendHttpHeadersCustomizer);
35-
return http.build();
35+
public SdaSecurityHeaders sdaSecurityHeaders() {
36+
return SdaSecurityType.FRONTEND_SECURITY::headers;
3637
}
3738
}

sda-commons-web-autoconfigure/src/main/java/org/sdase/commons/spring/boot/web/security/headers/HttpHeadersAdvice.java

Lines changed: 0 additions & 67 deletions
This file was deleted.

sda-commons-web-autoconfigure/src/main/java/org/sdase/commons/spring/boot/web/security/headers/RestfulApiSecurityConfiguration.java

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,10 @@
1010
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
1111
import org.springframework.context.annotation.Bean;
1212
import org.springframework.context.annotation.Configuration;
13-
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
14-
import org.springframework.security.web.SecurityFilterChain;
1513

1614
/**
17-
* This filter adds headers to the response that enhance the security of web applications. Usually
18-
* we do not provide web content from services. But we address the risks identified in the security
19-
* guide as:
15+
* This filter adds headers to the response that enhance the security of applications that serve
16+
* only REST APIs. The following risks are addressed:
2017
*
2118
* <ul>
2219
* <li>"Risk: Cross Site Scripting (XSS)"
@@ -28,12 +25,9 @@
2825
*/
2926
@Configuration
3027
public class RestfulApiSecurityConfiguration {
31-
3228
@Bean
3329
@ConditionalOnMissingBean(name = FrontendSecurityConfiguration.FRONTEND_SECURITY_ADVICE_BEAN_NAME)
34-
public SecurityFilterChain restfulApiHeadersAdvice(HttpSecurity http) throws Exception {
35-
// define specific headers for restful apis
36-
http.headers(HttpHeadersAdvice::addRestfulHttpHeadersCustomizer);
37-
return http.build();
30+
public SdaSecurityHeaders sdaSecurityHeaders() {
31+
return SdaSecurityType.RESTFUL_SECURITY::headers;
3832
}
3933
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/*
2+
* Copyright 2022- SDA SE Open Industry Solutions (https://www.sda.se)
3+
*
4+
* Use of this source code is governed by an MIT-style
5+
* license that can be found in the LICENSE file or at
6+
* https://opensource.org/licenses/MIT.
7+
*/
8+
package org.sdase.commons.spring.boot.web.security.headers;
9+
10+
import java.util.List;
11+
import org.springframework.security.web.header.Header;
12+
13+
public interface SdaSecurityHeaders {
14+
15+
List<Header> getSecurityHeaders();
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright 2022- SDA SE Open Industry Solutions (https://www.sda.se)
3+
*
4+
* Use of this source code is governed by an MIT-style
5+
* license that can be found in the LICENSE file or at
6+
* https://opensource.org/licenses/MIT.
7+
*/
8+
package org.sdase.commons.spring.boot.web.security.headers;
9+
10+
import static java.util.Arrays.asList;
11+
import static org.sdase.commons.spring.boot.web.security.headers.SdaSecurityType.Common.WEB_SECURITY_HEADERS;
12+
13+
import java.util.List;
14+
import java.util.stream.Stream;
15+
import org.springframework.security.web.header.Header;
16+
17+
public enum SdaSecurityType {
18+
RESTFUL_SECURITY(
19+
Stream.concat(
20+
WEB_SECURITY_HEADERS.stream(),
21+
Stream.of(
22+
new Header(
23+
"Content-Security-Policy",
24+
String.join(
25+
"; ",
26+
asList("default-src 'none'", "frame-ancestors 'none'", "sandbox")))))
27+
.toList()),
28+
29+
FRONTEND_SECURITY(
30+
Stream.concat(
31+
WEB_SECURITY_HEADERS.stream(),
32+
Stream.of(
33+
new Header(
34+
"Content-Security-Policy",
35+
String.join(
36+
"; ",
37+
asList(
38+
"default-src 'self'",
39+
"script-src 'self'",
40+
"img-src 'self'",
41+
"style-src 'self'",
42+
"font-src 'self'",
43+
"frame-src 'none'",
44+
"object-src 'none'")))))
45+
.toList());
46+
47+
private final List<Header> headers;
48+
49+
SdaSecurityType(List<Header> headers) {
50+
this.headers = headers;
51+
}
52+
53+
public List<Header> headers() {
54+
return headers;
55+
}
56+
57+
static class Common {
58+
private Common() {
59+
// just to hold WEB_SECURITY_HEADERS
60+
}
61+
62+
static final List<Header> WEB_SECURITY_HEADERS =
63+
List.of(
64+
new Header("X-Frame-Options", "DENY"),
65+
new Header("X-Content-Type-Options", "nosniff"),
66+
new Header("X-XSS-Protection", "1; mode=block"),
67+
new Header("Referrer-Policy", "same-origin"),
68+
new Header("X-Permitted-Cross-Domain-Policies", "none"));
69+
}
70+
}
Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,18 +35,32 @@
3535
import org.springframework.http.ResponseEntity;
3636
import org.springframework.test.context.ContextConfiguration;
3737

38-
/** This test class depends on the {@code @EnableFrontendSecurity} annotation being present. */
39-
@EnableFrontendSecurity
4038
@SpringBootTest(
4139
classes = SecurityTestApp.class,
4240
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
4341
@ContextConfiguration(initializers = DisableSdaAuthInitializer.class)
44-
class FrontendSecurityHeadersTest {
42+
abstract class AbstractSecurityHeadersTest {
4543
@LocalServerPort private int port;
4644

4745
@Autowired private TestRestTemplate client;
4846

49-
static Stream<Arguments> predefinedSecurityHeaders() {
47+
static Stream<Arguments> predefinedRestfulApiSecurityHeaders() {
48+
return Stream.of(
49+
// cache headers
50+
of("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate"),
51+
of("Pragma", "no-cache"),
52+
of("Expires", "0"),
53+
54+
// security headers
55+
of("X-Frame-Options", "DENY"),
56+
of("X-Content-Type-Options", "nosniff"),
57+
of("X-XSS-Protection", "1; mode=block"),
58+
of("Referrer-Policy", "same-origin"),
59+
of("X-Permitted-Cross-Domain-Policies", "none"),
60+
of("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; sandbox"));
61+
}
62+
63+
static Stream<Arguments> predefinedFrontendSecurityHeaders() {
5064
return Stream.of(
5165
// cache headers
5266
of("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate"),
@@ -73,10 +87,11 @@ static Stream<Arguments> predefinedSecurityHeaders() {
7387
"object-src 'none'"))));
7488
}
7589

90+
protected abstract Stream<Arguments> predefinedSecurityHeaders();
91+
7692
@ParameterizedTest
77-
@MethodSource("predefinedSecurityHeaders")
78-
void shouldAddFrontendSecurityHeaders(
79-
String predefinedHeaderName, String expectedPredefinedHeaderValue) {
93+
@MethodSource("predefinedRestfulApiSecurityHeaders")
94+
void shouldAddSecurityHeaders(String predefinedHeaderName, String expectedPredefinedHeaderValue) {
8095

8196
ResponseEntity<TestResource> actual =
8297
client.getForEntity(getServerBaseUrl() + "/api/resource", TestResource.class);
@@ -92,8 +107,8 @@ void shouldAddFrontendSecurityHeaders(
92107
}
93108

94109
@ParameterizedTest
95-
@MethodSource("predefinedSecurityHeaders")
96-
void shouldAllowOverwritingFrontendSecurityHeaders(String predefinedHeaderName) {
110+
@MethodSource("predefinedRestfulApiSecurityHeaders")
111+
void shouldAllowOverwritingHeaders(String predefinedHeaderName) {
97112
// for unknown reason, X-Frame-Options can't be modified
98113
assumeThat(predefinedHeaderName).isNotEqualTo("X-Frame-Options");
99114

0 commit comments

Comments
 (0)