Merge pull request #2234 from alek-sys/main

Fix secure headers duplication
This commit is contained in:
Ryan Baxter
2021-07-01 15:38:31 -04:00
committed by GitHub
4 changed files with 80 additions and 31 deletions

View File

@@ -92,39 +92,39 @@ public class SecureHeadersGatewayFilterFactory
List<String> disabled = properties.getDisable();
Config config = originalConfig.withDefaults(properties);
if (isEnabled(disabled, X_XSS_PROTECTION_HEADER)) {
headers.add(X_XSS_PROTECTION_HEADER, config.getXssProtectionHeader());
}
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
if (isEnabled(disabled, X_XSS_PROTECTION_HEADER)) {
headers.addIfAbsent(X_XSS_PROTECTION_HEADER, config.getXssProtectionHeader());
}
if (isEnabled(disabled, STRICT_TRANSPORT_SECURITY_HEADER)) {
headers.add(STRICT_TRANSPORT_SECURITY_HEADER, config.getStrictTransportSecurity());
}
if (isEnabled(disabled, STRICT_TRANSPORT_SECURITY_HEADER)) {
headers.addIfAbsent(STRICT_TRANSPORT_SECURITY_HEADER, config.getStrictTransportSecurity());
}
if (isEnabled(disabled, X_FRAME_OPTIONS_HEADER)) {
headers.add(X_FRAME_OPTIONS_HEADER, config.getFrameOptions());
}
if (isEnabled(disabled, X_FRAME_OPTIONS_HEADER)) {
headers.addIfAbsent(X_FRAME_OPTIONS_HEADER, config.getFrameOptions());
}
if (isEnabled(disabled, X_CONTENT_TYPE_OPTIONS_HEADER)) {
headers.add(X_CONTENT_TYPE_OPTIONS_HEADER, config.getContentTypeOptions());
}
if (isEnabled(disabled, X_CONTENT_TYPE_OPTIONS_HEADER)) {
headers.addIfAbsent(X_CONTENT_TYPE_OPTIONS_HEADER, config.getContentTypeOptions());
}
if (isEnabled(disabled, REFERRER_POLICY_HEADER)) {
headers.add(REFERRER_POLICY_HEADER, config.getReferrerPolicy());
}
if (isEnabled(disabled, REFERRER_POLICY_HEADER)) {
headers.addIfAbsent(REFERRER_POLICY_HEADER, config.getReferrerPolicy());
}
if (isEnabled(disabled, CONTENT_SECURITY_POLICY_HEADER)) {
headers.add(CONTENT_SECURITY_POLICY_HEADER, config.getContentSecurityPolicy());
}
if (isEnabled(disabled, CONTENT_SECURITY_POLICY_HEADER)) {
headers.addIfAbsent(CONTENT_SECURITY_POLICY_HEADER, config.getContentSecurityPolicy());
}
if (isEnabled(disabled, X_DOWNLOAD_OPTIONS_HEADER)) {
headers.add(X_DOWNLOAD_OPTIONS_HEADER, config.getDownloadOptions());
}
if (isEnabled(disabled, X_DOWNLOAD_OPTIONS_HEADER)) {
headers.addIfAbsent(X_DOWNLOAD_OPTIONS_HEADER, config.getDownloadOptions());
}
if (isEnabled(disabled, X_PERMITTED_CROSS_DOMAIN_POLICIES_HEADER)) {
headers.add(X_PERMITTED_CROSS_DOMAIN_POLICIES_HEADER, config.getPermittedCrossDomainPolicies());
}
return chain.filter(exchange);
if (isEnabled(disabled, X_PERMITTED_CROSS_DOMAIN_POLICIES_HEADER)) {
headers.addIfAbsent(X_PERMITTED_CROSS_DOMAIN_POLICIES_HEADER, config.getPermittedCrossDomainPolicies());
}
}));
}
@Override

View File

@@ -28,6 +28,7 @@ import org.springframework.cloud.gateway.test.BaseWebClientTests;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.reactive.function.client.ClientResponse;
@@ -73,6 +74,18 @@ public class SecureHeadersGatewayFilterFactoryTests extends BaseWebClientTests {
}).expectComplete().verify(DURATION);
}
@Test
public void addsSecureHeadersAfterResponseIsReceived() {
Mono<ClientResponse> result = webClient.patch().uri("/headers").header("Host", "www.secureheaders.org")
.contentType(MediaType.APPLICATION_JSON).bodyValue("{ \"X-Frame-Options\": \"sameorigin\" }")
.exchangeToMono(Mono::just);
StepVerifier.create(result).consumeNextWith(response -> {
assertThat(response.statusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.headers().header(X_FRAME_OPTIONS_HEADER)).containsOnly("sameorigin");
}).expectComplete().verify(DURATION);
}
@EnableAutoConfiguration
@SpringBootConfiguration
@Import(DefaultTestConfig.class)

View File

@@ -72,9 +72,9 @@ public class SecureHeadersGatewayFilterFactoryUnitTests {
new SecureHeadersProperties());
filter = filterFactory.apply(new Config());
filter.filter(exchange, filterChain);
filter.filter(exchange, filterChain).block();
ServerHttpResponse response = captor.getValue().getResponse();
ServerHttpResponse response = exchange.getResponse();
assertThat(response.getHeaders()).containsKeys(X_XSS_PROTECTION_HEADER, STRICT_TRANSPORT_SECURITY_HEADER,
X_FRAME_OPTIONS_HEADER, X_CONTENT_TYPE_OPTIONS_HEADER, REFERRER_POLICY_HEADER,
CONTENT_SECURITY_POLICY_HEADER, X_DOWNLOAD_OPTIONS_HEADER, X_PERMITTED_CROSS_DOMAIN_POLICIES_HEADER);
@@ -90,7 +90,7 @@ public class SecureHeadersGatewayFilterFactoryUnitTests {
SecureHeadersGatewayFilterFactory filterFactory = new SecureHeadersGatewayFilterFactory(properties);
filter = filterFactory.apply(new Config());
filter.filter(exchange, filterChain);
filter.filter(exchange, filterChain).block();
ServerHttpResponse response = captor.getValue().getResponse();
assertThat(response.getHeaders()).doesNotContainKeys(X_XSS_PROTECTION_HEADER, STRICT_TRANSPORT_SECURITY_HEADER,
@@ -109,9 +109,9 @@ public class SecureHeadersGatewayFilterFactoryUnitTests {
config.setReferrerPolicy("referrer");
filter = filterFactory.apply(config);
filter.filter(exchange, filterChain);
filter.filter(exchange, filterChain).block();
ServerHttpResponse response = captor.getValue().getResponse();
ServerHttpResponse response = exchange.getResponse();
assertThat(response.getHeaders()).containsKeys(X_XSS_PROTECTION_HEADER, STRICT_TRANSPORT_SECURITY_HEADER,
X_FRAME_OPTIONS_HEADER, X_CONTENT_TYPE_OPTIONS_HEADER, REFERRER_POLICY_HEADER,
CONTENT_SECURITY_POLICY_HEADER, X_DOWNLOAD_OPTIONS_HEADER, X_PERMITTED_CROSS_DOMAIN_POLICIES_HEADER);
@@ -132,6 +132,31 @@ public class SecureHeadersGatewayFilterFactoryUnitTests {
}
@Test
public void doesNotDuplicateHeaders() {
String originalHeaderValue = "original-header-value";
SecureHeadersGatewayFilterFactory filterFactory = new SecureHeadersGatewayFilterFactory(
new SecureHeadersProperties());
Config config = new Config();
String[] headers = { X_XSS_PROTECTION_HEADER, STRICT_TRANSPORT_SECURITY_HEADER, X_FRAME_OPTIONS_HEADER,
X_CONTENT_TYPE_OPTIONS_HEADER, REFERRER_POLICY_HEADER, CONTENT_SECURITY_POLICY_HEADER,
X_DOWNLOAD_OPTIONS_HEADER, X_PERMITTED_CROSS_DOMAIN_POLICIES_HEADER };
for (String header : headers) {
filter = filterFactory.apply(config);
MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost").build();
exchange = MockServerWebExchange.from(request);
exchange.getResponse().getHeaders().set(header, originalHeaderValue);
filter.filter(exchange, filterChain).block();
ServerHttpResponse response = captor.getValue().getResponse();
assertThat(response.getHeaders().get(header)).containsOnly(originalHeaderValue);
}
}
@Test
public void toStringFormat() {
GatewayFilter filter = new SecureHeadersGatewayFilterFactory(new SecureHeadersProperties()).apply(new Config());

View File

@@ -73,6 +73,17 @@ public class HttpBinCompatibleController {
return result;
}
@RequestMapping(path = "/headers", method = RequestMethod.PATCH)
public ResponseEntity<Map<String, Object>> headersPatch(ServerWebExchange exchange,
@RequestBody Map<String, String> headersToAdd) {
Map<String, Object> result = new HashMap<>();
result.put("headers", getHeaders(exchange));
ResponseEntity.BodyBuilder responseEntity = ResponseEntity.status(HttpStatus.OK);
headersToAdd.forEach(responseEntity::header);
return responseEntity.body(result);
}
@RequestMapping(path = "/multivalueheaders", method = { RequestMethod.GET, RequestMethod.POST },
produces = MediaType.APPLICATION_JSON_VALUE)
public Map<String, Object> multiValueHeaders(ServerWebExchange exchange) {