Improper Input Validation in org.springframework.security:spring-security-core, org.springframework.security:spring-security-core , and org.springframework:spring-core
Research is free — Hunters explains how the bug works, the root-cause code pattern, how the fix addresses it, and how to test whether a target is affected, in chat. Investigate & write exploit is a paid run — the engine reads the advisory and fix commits, then builds and validates a working proof-of-concept exploit with reproduction steps.
Affected versions
Details
Spring Security (Spring Security 4.1.x before 4.1.5, 4.2.x before 4.2.4, and 5.0.x before 5.0.1; and Spring Framework 4.3.x before 4.3.14 and 5.0.x before 5.0.3) does not consider URL path parameters when processing security constraints. By adding a URL path parameter with special encodings, an attacker may be able to bypass a security constraint. The root cause of this issue is a lack of clarity regarding the handling of path parameters in the Servlet Specification. Some Servlet containers include path parameters in the value returned for getPathInfo() and some do not. Spring Security uses the value returned by getPathInfo() as part of the process of mapping requests to security constraints. In this particular attack, different character encodings used in path parameters allows secured Spring MVC static resource URLs to be bypassed.
The fix
Allow interceptors to add existing header values
spring-web/src/main/java/org/springframework/web/client/RestTemplate.java+4 −1
@@ -21,6 +21,7 @@import java.net.URI;import java.util.ArrayList;import java.util.Collections;+import java.util.LinkedList;import java.util.List;import java.util.Map;import java.util.Set;@@ -909,7 +910,9 @@ public void doWithRequest(ClientHttpRequest httpRequest) throws IOException {HttpHeaders httpHeaders = httpRequest.getHeaders();HttpHeaders requestHeaders = this.requestEntity.getHeaders();if (!requestHeaders.isEmpty()) {-httpHeaders.putAll(requestHeaders);+for (Map.Entry<String, List<String>> entry : requestHeaders.entrySet()) {+httpHeaders.put(entry.getKey(), new LinkedList<>(entry.getValue()));+}}if (httpHeaders.getContentLength() < 0) {httpHeaders.setContentLength(0L);
spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java+29 −0
@@ -39,12 +39,15 @@import org.springframework.http.ResponseEntity;import org.springframework.http.client.ClientHttpRequest;import org.springframework.http.client.ClientHttpRequestFactory;+import org.springframework.http.client.ClientHttpRequestInterceptor;import org.springframework.http.client.ClientHttpResponse;import org.springframework.http.converter.GenericHttpMessageConverter;import org.springframework.http.converter.HttpMessageConverter;import org.springframework.util.StreamUtils;import org.springframework.web.util.DefaultUriBuilderFactory;+import static org.hamcrest.MatcherAssert.assertThat;+import static org.hamcrest.collection.IsIterableContainingInOrder.contains;import static org.junit.Assert.*;import static org.mockito.BDDMockito.*;import static org.springframework.http.HttpMethod.POST;@@ -822,4 +825,30 @@ public void exchangeParameterizedType() throws Exception {verify(response).close();}+@Test // SPR-15066+public void requestInterceptorCanAddExistingHeaderValue() throws Exception {+ClientHttpRequestInterceptor interceptor = (request, body, execution) -> {+request.getHeaders().add("MyHeader", "MyInterceptorValue");+return execution.execute(request, body);+};+template.setInterceptors(Collections.singletonList(interceptor));++given(requestFactory.createRequest(new URI("http://example.com"), HttpMethod.POST)).willReturn(request);+HttpHeaders requestHeaders = new HttpHeaders();+given(request.getHeaders()).willReturn(requestHeaders);+given(request.execute()).willReturn(response);+given(errorHandler.hasError(response)).willReturn(false);+HttpStatus status = HttpStatus.OK;+given(response.getStatusCode()).willReturn(status);+given(response.getStatusText()).willReturn(status.getReasonPhrase());++HttpHeaders entityHeaders = new HttpHeaders();+entityHeaders.add("MyHeader", "MyEntityValue");+HttpEntity<Void> entity = new HttpEntity<>(null, entityHeaders);+template.exchange("http://example.com", HttpMethod.POST, entity, Void.class);+assertThat(requestHeaders.get("MyHeader"), contains("MyEntityValue", "MyInterceptorValue"));++verify(response).close();+}+}
Allow interceptors to add existing header values
spring-web/src/main/java/org/springframework/web/client/RestTemplate.java+7 −2
@@ -922,6 +922,7 @@ public void doWithRequest(ClientHttpRequest httpRequest) throws IOException {Class<?> requestBodyClass = requestBody.getClass();Type requestBodyType = (this.requestEntity instanceof RequestEntity ?((RequestEntity<?>)this.requestEntity).getType() : requestBodyClass);+HttpHeaders httpHeaders = httpRequest.getHeaders();HttpHeaders requestHeaders = this.requestEntity.getHeaders();MediaType requestContentType = requestHeaders.getContentType();for (HttpMessageConverter<?> messageConverter : getMessageConverters()) {@@ -930,7 +931,9 @@ public void doWithRequest(ClientHttpRequest httpRequest) throws IOException {(GenericHttpMessageConverter<Object>) messageConverter;if (genericConverter.canWrite(requestBodyType, requestBodyClass, requestContentType)) {if (!requestHeaders.isEmpty()) {-httpRequest.getHeaders().putAll(requestHeaders);+for (Map.Entry<String, List<String>> entry : requestHeaders.entrySet()) {+httpHeaders.put(entry.getKey(), new LinkedList<>(entry.getValue()));+}}if (logger.isDebugEnabled()) {if (requestContentType != null) {@@ -948,7 +951,9 @@ public void doWithRequest(ClientHttpRequest httpRequest) throws IOException {}else if (messageConverter.canWrite(requestBodyClass, requestContentType)) {if (!requestHeaders.isEmpty()) {-httpRequest.getHeaders().putAll(requestHeaders);+for (Map.Entry<String, List<String>> entry : requestHeaders.entrySet()) {+httpHeaders.put(entry.getKey(), new LinkedList<>(entry.getValue()));+}}if (logger.isDebugEnabled()) {if (requestContentType != null) {
spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java+32 −1
@@ -826,7 +826,7 @@ public void exchangeParameterizedType() throws Exception {}@Test // SPR-15066-public void requestInterceptorCanAddExistingHeaderValue() throws Exception {+public void requestInterceptorCanAddExistingHeaderValueWithoutBody() throws Exception {ClientHttpRequestInterceptor interceptor = (request, body, execution) -> {request.getHeaders().add("MyHeader", "MyInterceptorValue");return execution.execute(request, body);@@ -851,4 +851,35 @@ public void requestInterceptorCanAddExistingHeaderValue() throws Exception {verify(response).close();}+@Test // SPR-15066+public void requestInterceptorCanAddExistingHeaderValueWithBody() throws Exception {+ClientHttpRequestInterceptor interceptor = (request, body, execution) -> {+request.getHeaders().add("MyHeader", "MyInterceptorValue");+return execution.execute(request, body);+};+template.setInterceptors(Collections.singletonList(interceptor));++MediaType contentType = MediaType.TEXT_PLAIN;+given(converter.canWrite(String.class, contentType)).willReturn(true);+given(requestFactory.createRequest(new URI("http://example.com"), HttpMethod.POST)).willReturn(request);+String helloWorld = "Hello World";+HttpHeaders requestHeaders = new HttpHeaders();+given(request.getHeaders()).willReturn(requestHeaders);+converter.write(helloWorld, contentType, request);+given(request.execute()).willReturn(response);+given(errorHandler.hasError(response)).willReturn(false);+HttpStatus status = HttpStatus.OK;+given(response.getStatusCode()).willReturn(status);+given(response.getStatusText()).willReturn(status.getReasonPhrase());++HttpHeaders entityHeaders = new HttpHeaders();+entityHeaders.setContentType(contentType);+entityHeaders.add("MyHeader", "MyEntityValue");+HttpEntity<String> entity = new HttpEntity<>(helloWorld, entityHeaders);+template.exchange("http://example.com", HttpMethod.POST, entity, Void.class);+assertThat(requestHeaders.get("MyHeader"), contains("MyEntityValue", "MyInterceptorValue"));++verify(response).close();+}+}
Allow interceptors to add existing header values
spring-web/src/main/java/org/springframework/web/client/RestTemplate.java+7 −2
@@ -852,6 +852,7 @@ public void doWithRequest(ClientHttpRequest httpRequest) throws IOException {Class<?> requestBodyClass = requestBody.getClass();Type requestBodyType = (this.requestEntity instanceof RequestEntity ?((RequestEntity<?>)this.requestEntity).getType() : requestBodyClass);+HttpHeaders httpHeaders = httpRequest.getHeaders();HttpHeaders requestHeaders = this.requestEntity.getHeaders();MediaType requestContentType = requestHeaders.getContentType();for (HttpMessageConverter<?> messageConverter : getMessageConverters()) {@@ -859,7 +860,9 @@ public void doWithRequest(ClientHttpRequest httpRequest) throws IOException {GenericHttpMessageConverter<Object> genericMessageConverter = (GenericHttpMessageConverter<Object>) messageConverter;if (genericMessageConverter.canWrite(requestBodyType, requestBodyClass, requestContentType)) {if (!requestHeaders.isEmpty()) {-httpRequest.getHeaders().putAll(requestHeaders);+for (Map.Entry<String, List<String>> entry : requestHeaders.entrySet()) {+httpHeaders.put(entry.getKey(), new LinkedList<String>(entry.getValue()));+}}if (logger.isDebugEnabled()) {if (requestContentType != null) {@@ -878,7 +881,9 @@ public void doWithRequest(ClientHttpRequest httpRequest) throws IOException {}else if (messageConverter.canWrite(requestBodyClass, requestContentType)) {if (!requestHeaders.isEmpty()) {-httpRequest.getHeaders().putAll(requestHeaders);+for (Map.Entry<String, List<String>> entry : requestHeaders.entrySet()) {+httpHeaders.put(entry.getKey(), new LinkedList<String>(entry.getValue()));+}}if (logger.isDebugEnabled()) {if (requestContentType != null) {
spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java+32 −1
@@ -836,7 +836,7 @@ public void exchangeParameterizedType() throws Exception {}@Test // SPR-15066-public void requestInterceptorCanAddExistingHeaderValue() throws Exception {+public void requestInterceptorCanAddExistingHeaderValueWithoutBody() throws Exception {ClientHttpRequestInterceptor interceptor = (request, body, execution) -> {request.getHeaders().add("MyHeader", "MyInterceptorValue");return execution.execute(request, body);@@ -861,4 +861,35 @@ public void requestInterceptorCanAddExistingHeaderValue() throws Exception {verify(response).close();}+@Test // SPR-15066+public void requestInterceptorCanAddExistingHeaderValueWithBody() throws Exception {+ClientHttpRequestInterceptor interceptor = (request, body, execution) -> {+request.getHeaders().add("MyHeader", "MyInterceptorValue");+return execution.execute(request, body);+};+template.setInterceptors(Collections.singletonList(interceptor));++MediaType contentType = MediaType.TEXT_PLAIN;+given(converter.canWrite(String.class, contentType)).willReturn(true);+given(requestFactory.createRequest(new URI("http://example.com"), HttpMethod.POST)).willReturn(request);+String helloWorld = "Hello World";+HttpHeaders requestHeaders = new HttpHeaders();+given(request.getHeaders()).willReturn(requestHeaders);+converter.write(helloWorld, contentType, request);+given(request.execute()).willReturn(response);+given(errorHandler.hasError(response)).willReturn(false);+HttpStatus status = HttpStatus.OK;+given(response.getStatusCode()).willReturn(status);+given(response.getStatusText()).willReturn(status.getReasonPhrase());++HttpHeaders entityHeaders = new HttpHeaders();+entityHeaders.setContentType(contentType);+entityHeaders.add("MyHeader", "MyEntityValue");+HttpEntity<String> entity = new HttpEntity<>(helloWorld, entityHeaders);+template.exchange("http://example.com", HttpMethod.POST, entity, Void.class);+assertThat(requestHeaders.get("MyHeader"), contains("MyEntityValue", "MyInterceptorValue"));++verify(response).close();+}+}
References
- ADVISORYhttps://nvd.nist.gov/vuln/detail/CVE-2018-1199
- WEBhttps://github.com/spring-projects/spring-framework/commit/554662ebab87af97ba25d0c9f5449c7acda8df9c
- WEBhttps://github.com/spring-projects/spring-framework/commit/73a81f98d40eb6f5faa91aceb868db53fae2a94b
- WEBhttps://github.com/spring-projects/spring-framework/commit/e6e6b8f4adcad99d133de97fcfac5ae5dd14153c
- WEBhttps://github.com/spring-projects/spring-security/commit/0eef5b4b425ab42b9fa0fde1a3f36a37b92558f
- WEBhttps://github.com/spring-projects/spring-security/commit/65da28e4bf62f58fb130ba727cbbd621b44a36d
- WEBhttps://github.com/spring-projects/spring-security/commit/cb8041ba67635edafcc934498ef82707157fd22
- WEBhttps://access.redhat.com/errata/RHSA-2018:2405
- ADVISORYhttps://github.com/advisories/GHSA-v596-fwhq-8x48
- WEBhttps://lists.apache.org/thread.html/4ed49b103f64a0cecb38064f26cbf1389afc12124653da2d35166dbe@%3Cissues.activemq.apache.org%3E
- WEBhttps://lists.apache.org/thread.html/ab825fcade0b49becfa30235b3d54f4a51bb74ea96b6c9adb5d1378c@%3Cissues.activemq.apache.org%3E
- WEBhttps://lists.apache.org/thread.html/dcf8599b80e43a6b60482607adb76c64672772dc2d9209ae2170f369@%3Cissues.activemq.apache.org%3E