diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/EnvironmentController.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/EnvironmentController.java index 6a7f51b7..9a5e5310 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/EnvironmentController.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/EnvironmentController.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2019 the original author or authors. + * Copyright 2013-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,6 +35,7 @@ import org.yaml.snakeyaml.nodes.Tag; import org.springframework.cloud.config.environment.Environment; import org.springframework.cloud.config.environment.EnvironmentMediaType; import org.springframework.cloud.config.environment.PropertySource; +import org.springframework.cloud.config.server.support.PathUtils; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -130,8 +131,8 @@ public class EnvironmentController { public Environment getEnvironment(String name, String profiles, String label, boolean includeOrigin) { - name = Environment.normalize(name); - label = Environment.normalize(label); + name = normalize(name); + label = normalize(label); Environment environment = this.repository.findOne(name, profiles, label, includeOrigin); if (!this.acceptEmpty @@ -141,6 +142,13 @@ public class EnvironmentController { return environment; } + private String normalize(String part) { + if (PathUtils.isInvalidEncodedLocation(part)) { + throw new InvalidEnvironmentRequestException("Invalid request"); + } + return Environment.normalize(part); + } + @RequestMapping("/{name}-{profiles}.properties") public ResponseEntity properties(@PathVariable String name, @PathVariable String profiles, diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/InvalidEnvironmentRequestException.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/InvalidEnvironmentRequestException.java new file mode 100644 index 00000000..f390be7a --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/InvalidEnvironmentRequestException.java @@ -0,0 +1,33 @@ +/* + * Copyright 2018-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.config.server.environment; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +/** + * @author Chidambaram Sundaram + */ +@SuppressWarnings("serial") +@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "Invalid Request") +public class InvalidEnvironmentRequestException extends RuntimeException { + + public InvalidEnvironmentRequestException(String string) { + super(string); + } + +} diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/resource/GenericResourceRepository.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/resource/GenericResourceRepository.java index 46d6b02a..87b89764 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/resource/GenericResourceRepository.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/resource/GenericResourceRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2019 the original author or authors. + * Copyright 2013-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,20 +17,15 @@ package org.springframework.cloud.config.server.resource; import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; import java.util.Collection; import java.util.LinkedHashSet; import java.util.Set; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - import org.springframework.cloud.config.server.environment.SearchPathLocator; +import org.springframework.cloud.config.server.support.PathUtils; import org.springframework.context.ResourceLoaderAware; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; -import org.springframework.util.ResourceUtils; import org.springframework.util.StringUtils; /** @@ -41,8 +36,6 @@ import org.springframework.util.StringUtils; public class GenericResourceRepository implements ResourceRepository, ResourceLoaderAware { - private static final Log logger = LogFactory.getLog(GenericResourceRepository.class); - private ResourceLoader resourceLoader; private SearchPathLocator service; @@ -66,11 +59,12 @@ public class GenericResourceRepository try { for (int i = locations.length; i-- > 0;) { String location = locations[i]; - if (isInvalidEncodedLocation(location)) { + if (PathUtils.isInvalidEncodedLocation(location)) { continue; } for (String local : getProfilePaths(profile, path)) { - if (!isInvalidPath(local) && !isInvalidEncodedPath(local)) { + if (!PathUtils.isInvalidPath(local) + && !PathUtils.isInvalidEncodedPath(local)) { Resource file = this.resourceLoader.getResource(location) .createRelative(local); if (file.exists() && file.isReadable()) { @@ -88,41 +82,6 @@ public class GenericResourceRepository throw new NoSuchResourceException("Not found: " + path); } - /** - * Check whether the given location contains invalid escape sequences. - * @param location the location to validate - * @return {@code true} if the path is invalid, {@code false} otherwise - */ - private boolean isInvalidEncodedLocation(String location) { - if (location.contains("%")) { - try { - // Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 - // chars - String decodedPath = URLDecoder.decode(location, "UTF-8"); - if (isInvalidLocation(decodedPath)) { - return true; - } - decodedPath = processPath(decodedPath); - if (isInvalidLocation(decodedPath)) { - return true; - } - } - catch (IllegalArgumentException | UnsupportedEncodingException ex) { - // Should never happen... - } - } - return isInvalidLocation(location); - } - - private boolean isInvalidLocation(String location) { - boolean isInvalid = location.contains(".."); - - if (isInvalid && logger.isWarnEnabled()) { - logger.warn("Location contains \"..\""); - } - return isInvalid; - } - private Collection getProfilePaths(String profiles, String path) { Set paths = new LinkedHashSet<>(); for (String profile : StringUtils.commaDelimitedListToSet(profiles)) { @@ -146,133 +105,4 @@ public class GenericResourceRepository return paths; } - /** - * Check whether the given path contains invalid escape sequences. - * @param path the path to validate - * @return {@code true} if the path is invalid, {@code false} otherwise - */ - private boolean isInvalidEncodedPath(String path) { - if (path.contains("%")) { - try { - // Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 - // chars - String decodedPath = URLDecoder.decode(path, "UTF-8"); - if (isInvalidPath(decodedPath)) { - return true; - } - decodedPath = processPath(decodedPath); - if (isInvalidPath(decodedPath)) { - return true; - } - } - catch (IllegalArgumentException | UnsupportedEncodingException ex) { - // Should never happen... - } - } - return false; - } - - /** - * Process the given resource path. - *

- * The default implementation replaces: - *

    - *
  • Backslash with forward slash. - *
  • Duplicate occurrences of slash with a single slash. - *
  • Any combination of leading slash and control characters (00-1F and 7F) with a - * single "/" or "". For example {@code " / // foo/bar"} becomes {@code "/foo/bar"}. - *
- * @param path path to process - * @return the processed path - * @since 3.2.12 - */ - protected String processPath(String path) { - path = StringUtils.replace(path, "\\", "/"); - path = cleanDuplicateSlashes(path); - return cleanLeadingSlash(path); - } - - private String cleanDuplicateSlashes(String path) { - StringBuilder sb = null; - char prev = 0; - for (int i = 0; i < path.length(); i++) { - char curr = path.charAt(i); - try { - if ((curr == '/') && (prev == '/')) { - if (sb == null) { - sb = new StringBuilder(path.substring(0, i)); - } - continue; - } - if (sb != null) { - sb.append(path.charAt(i)); - } - } - finally { - prev = curr; - } - } - return sb != null ? sb.toString() : path; - } - - private String cleanLeadingSlash(String path) { - boolean slash = false; - for (int i = 0; i < path.length(); i++) { - if (path.charAt(i) == '/') { - slash = true; - } - else if (path.charAt(i) > ' ' && path.charAt(i) != 127) { - if (i == 0 || (i == 1 && slash)) { - return path; - } - return (slash ? "/" + path.substring(i) : path.substring(i)); - } - } - return (slash ? "/" : ""); - } - - /** - * Identifies invalid resource paths. By default rejects: - *
    - *
  • Paths that contain "WEB-INF" or "META-INF" - *
  • Paths that contain "../" after a call to - * {@link org.springframework.util.StringUtils#cleanPath}. - *
  • Paths that represent a {@link org.springframework.util.ResourceUtils#isUrl - * valid URL} or would represent one after the leading slash is removed. - *
- *

- * Note: this method assumes that leading, duplicate '/' or control - * characters (e.g. white space) have been trimmed so that the path starts predictably - * with a single '/' or does not have one. - * @param path the path to validate - * @return {@code true} if the path is invalid, {@code false} otherwise - * @since 3.0.6 - */ - protected boolean isInvalidPath(String path) { - if (path.contains("WEB-INF") || path.contains("META-INF")) { - if (logger.isWarnEnabled()) { - logger.warn("Path with \"WEB-INF\" or \"META-INF\": [" + path + "]"); - } - return true; - } - if (path.contains(":/")) { - String relativePath = (path.charAt(0) == '/' ? path.substring(1) : path); - if (ResourceUtils.isUrl(relativePath) || relativePath.startsWith("url:")) { - if (logger.isWarnEnabled()) { - logger.warn( - "Path represents URL or has \"url:\" prefix: [" + path + "]"); - } - return true; - } - } - if (path.contains("..") && StringUtils.cleanPath(path).contains("../")) { - if (logger.isWarnEnabled()) { - logger.warn("Path contains \"../\" after call to StringUtils#cleanPath: [" - + path + "]"); - } - return true; - } - return false; - } - } diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/support/PathUtils.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/support/PathUtils.java new file mode 100644 index 00000000..6c2488c1 --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/support/PathUtils.java @@ -0,0 +1,206 @@ +/* + * Copyright 2013-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.config.server.support; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.util.ResourceUtils; +import org.springframework.util.StringUtils; + +public abstract class PathUtils { + + private static final Log logger = LogFactory.getLog(PathUtils.class); + + private PathUtils() { + } + + /** + * Check whether the given location contains invalid escape sequences. + * @param location the location to validate + * @return {@code true} if the path is invalid, {@code false} otherwise + */ + public static boolean isInvalidEncodedLocation(String location) { + if (StringUtils.isEmpty(location)) { + return false; + } + if (location.contains("%")) { + try { + // Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 + // chars + String decodedPath = URLDecoder.decode(location, "UTF-8"); + if (isInvalidLocation(decodedPath)) { + return true; + } + decodedPath = processPath(decodedPath); + if (isInvalidLocation(decodedPath)) { + return true; + } + } + catch (IllegalArgumentException | UnsupportedEncodingException ex) { + // Should never happen... + } + } + return isInvalidLocation(location); + } + + private static boolean isInvalidLocation(String location) { + boolean isInvalid = location.contains(".."); + + if (isInvalid && logger.isWarnEnabled()) { + logger.warn("Location contains \"..\""); + } + if (!isInvalid) { + isInvalid = location.contains("#"); + if (isInvalid && logger.isWarnEnabled()) { + logger.warn("Location contains \"#\""); + } + } + + return isInvalid; + } + + /** + * Check whether the given path contains invalid escape sequences. + * @param path the path to validate + * @return {@code true} if the path is invalid, {@code false} otherwise + */ + public static boolean isInvalidEncodedPath(String path) { + if (path.contains("%")) { + try { + // Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 + // chars + String decodedPath = URLDecoder.decode(path, "UTF-8"); + if (isInvalidPath(decodedPath)) { + return true; + } + decodedPath = processPath(decodedPath); + if (isInvalidPath(decodedPath)) { + return true; + } + } + catch (IllegalArgumentException | UnsupportedEncodingException ex) { + // Should never happen... + } + } + return false; + } + + /** + * Process the given resource path. + *

+ * The default implementation replaces: + *

    + *
  • Backslash with forward slash. + *
  • Duplicate occurrences of slash with a single slash. + *
  • Any combination of leading slash and control characters (00-1F and 7F) with a + * single "/" or "". For example {@code " / // foo/bar"} becomes {@code "/foo/bar"}. + *
+ * @since 3.2.12 + */ + protected static String processPath(String path) { + path = StringUtils.replace(path, "\\", "/"); + path = cleanDuplicateSlashes(path); + return cleanLeadingSlash(path); + } + + private static String cleanDuplicateSlashes(String path) { + StringBuilder sb = null; + char prev = 0; + for (int i = 0; i < path.length(); i++) { + char curr = path.charAt(i); + try { + if ((curr == '/') && (prev == '/')) { + if (sb == null) { + sb = new StringBuilder(path.substring(0, i)); + } + continue; + } + if (sb != null) { + sb.append(path.charAt(i)); + } + } + finally { + prev = curr; + } + } + return sb != null ? sb.toString() : path; + } + + private static String cleanLeadingSlash(String path) { + boolean slash = false; + for (int i = 0; i < path.length(); i++) { + if (path.charAt(i) == '/') { + slash = true; + } + else if (path.charAt(i) > ' ' && path.charAt(i) != 127) { + if (i == 0 || (i == 1 && slash)) { + return path; + } + return (slash ? "/" + path.substring(i) : path.substring(i)); + } + } + return (slash ? "/" : ""); + } + + /** + * Identifies invalid resource paths. By default rejects: + *
    + *
  • Paths that contain "WEB-INF" or "META-INF" + *
  • Paths that contain "../" after a call to {@link StringUtils#cleanPath}. + *
  • Paths that represent a {@link org.springframework.util.ResourceUtils#isUrl + * valid URL} or would represent one after the leading slash is removed. + *
+ *

+ * Note: this method assumes that leading, duplicate '/' or control + * characters (e.g. white space) have been trimmed so that the path starts predictably + * with a single '/' or does not have one. + * @param path the path to validate + * @return {@code true} if the path is invalid, {@code false} otherwise + * @since 3.0.6 + */ + public static boolean isInvalidPath(String path) { + if (path.contains("WEB-INF") || path.contains("META-INF")) { + if (logger.isWarnEnabled()) { + logger.warn("Path with \"WEB-INF\" or \"META-INF\": [" + path + "]"); + } + return true; + } + if (path.contains(":/")) { + String relativePath = (path.charAt(0) == '/' ? path.substring(1) : path); + if (ResourceUtils.isUrl(relativePath) || relativePath.startsWith("url:")) { + if (logger.isWarnEnabled()) { + logger.warn( + "Path represents URL or has \"url:\" prefix: [" + path + "]"); + } + return true; + } + } + if (path.contains("..") && StringUtils.cleanPath(path).contains("../")) { + if (logger.isWarnEnabled()) { + logger.warn("Path contains \"../\" after call to StringUtils#cleanPath: [" + + path + "]"); + } + return true; + } + return false; + } + +} diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/EnvironmentControllerTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/EnvironmentControllerTests.java index 786e4446..312b17ee 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/EnvironmentControllerTests.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/EnvironmentControllerTests.java @@ -680,4 +680,34 @@ public class EnvironmentControllerTests { assertThat(result.getResponse().getErrorMessage()).isEqualTo("Cannot construct"); } + @Test + public void labelWithPreviousDirectory() { + expected.expect(InvalidEnvironmentRequestException.class); + this.controller.labelled("foo", "bar", "..(_).."); + } + + @Test + public void labelWithPreviousDirectoryEncodedParenthesis() { + expected.expect(InvalidEnvironmentRequestException.class); + this.controller.labelled("foo", "bar", "..%28_%29.."); + } + + @Test + public void labelWithPreviousDirectoryAllEncoded() { + expected.expect(InvalidEnvironmentRequestException.class); + this.controller.labelled("foo", "bar", "%2E%2E%28%5F%29%2E%2E"); + } + + @Test + public void nameWithPound() { + expected.expect(InvalidEnvironmentRequestException.class); + this.controller.labelled("foo#", "bar", "mylabel"); + } + + @Test + public void nameWithPoundEncoded() { + expected.expect(InvalidEnvironmentRequestException.class); + this.controller.labelled("foo%23", "bar", "mylabel"); + } + }