diff --git a/spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistrySupport.java b/spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistrySupport.java
index da6b9232919..fcde6786e0b 100644
--- a/spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistrySupport.java
+++ b/spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistrySupport.java
@@ -79,12 +79,15 @@ import org.springframework.util.ClassUtils;
/**
* Base implementation of the {@link PropertyEditorRegistry} interface.
- * Provides management of default editors and custom editors.
- * Mainly serves as base class for {@link BeanWrapperImpl}.
+ *
+ *
Provides management of default editors and custom editors.
+ *
+ *
Mainly serves as base class for {@link BeanWrapperImpl}.
*
* @author Juergen Hoeller
* @author Rob Harrop
* @author Sebastien Deleuze
+ * @author Sam Brannen
* @since 1.2.6
* @see java.beans.PropertyEditorManager
* @see java.beans.PropertyEditorSupport#setAsText
@@ -92,6 +95,15 @@ import org.springframework.util.ClassUtils;
*/
public class PropertyEditorRegistrySupport implements PropertyEditorRegistry {
+ /**
+ * The maximum number of {@code [key]} segments that {@link #addStrippedPropertyPaths}
+ * will process in a single property path, to avoid excessive recursion and the
+ * resulting exponential blowup in the number of generated stripped paths for
+ * property paths with a large number of {@code [key]} segments.
+ * @since 7.1
+ */
+ private static final int MAX_STRIPPED_PROPERTY_PATH_DEPTH = 8;
+
private @Nullable ConversionService conversionService;
private boolean defaultEditorsActive = false;
@@ -500,12 +512,23 @@ public class PropertyEditorRegistrySupport implements PropertyEditorRegistry {
/**
* Add property paths with all variations of stripped keys and/or indexes.
- * Invokes itself recursively with nested paths.
+ *
Invokes itself recursively with nested paths, bounded to a nesting depth
+ * of {@link #MAX_STRIPPED_PROPERTY_PATH_DEPTH}.
* @param strippedPaths the result list to add to
* @param nestedPath the current nested path
* @param propertyPath the property path to check for keys/indexes to strip
*/
private void addStrippedPropertyPaths(List strippedPaths, String nestedPath, String propertyPath) {
+ addStrippedPropertyPaths(strippedPaths, nestedPath, propertyPath, 0);
+ }
+
+ private void addStrippedPropertyPaths(
+ List strippedPaths, String nestedPath, String propertyPath, int depth) {
+
+ if (depth >= MAX_STRIPPED_PROPERTY_PATH_DEPTH) {
+ // Avoid excessive recursion.
+ return;
+ }
int startIndex = propertyPath.indexOf(PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR);
if (startIndex != -1) {
int endIndex = propertyPath.indexOf(PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR);
@@ -516,9 +539,9 @@ public class PropertyEditorRegistrySupport implements PropertyEditorRegistry {
// Strip the first key.
strippedPaths.add(nestedPath + prefix + suffix);
// Search for further keys to strip, with the first key stripped.
- addStrippedPropertyPaths(strippedPaths, nestedPath + prefix, suffix);
+ addStrippedPropertyPaths(strippedPaths, nestedPath + prefix, suffix, depth + 1);
// Search for further keys to strip, with the first key not stripped.
- addStrippedPropertyPaths(strippedPaths, nestedPath + prefix + key, suffix);
+ addStrippedPropertyPaths(strippedPaths, nestedPath + prefix + key, suffix, depth + 1);
}
}
}
diff --git a/spring-beans/src/test/java/org/springframework/beans/PropertyEditorRegistrySupportTests.java b/spring-beans/src/test/java/org/springframework/beans/PropertyEditorRegistrySupportTests.java
new file mode 100644
index 00000000000..b596934ac43
--- /dev/null
+++ b/spring-beans/src/test/java/org/springframework/beans/PropertyEditorRegistrySupportTests.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2002-present 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.beans;
+
+import java.beans.PropertyEditor;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+import org.springframework.beans.propertyeditors.CustomNumberEditor;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link PropertyEditorRegistrySupport}.
+ *
+ * @author Sam Brannen
+ * @since 7.1
+ */
+class PropertyEditorRegistrySupportTests {
+
+ /**
+ * Matches the private {@code MAX_STRIPPED_PROPERTY_PATH_DEPTH} constant in
+ * {@link PropertyEditorRegistrySupport}.
+ */
+ private static final int MAX_DEPTH = 8;
+
+ private final PropertyEditorRegistrySupport registry = new PropertyEditorRegistrySupport();
+
+ private final PropertyEditor editor = new CustomNumberEditor(Integer.class, true);
+
+
+ @Test
+ void findCustomEditorMatchesStrippedPathAtMaxSupportedNestingDepth() {
+ registry.registerCustomEditor(null, "list", this.editor);
+
+ String propertyPath = "list" + "[0]".repeat(MAX_DEPTH);
+ assertThat(registry.findCustomEditor(null, propertyPath)).isSameAs(this.editor);
+ }
+
+ @Test
+ void findCustomEditorDoesNotMatchStrippedPathBeyondMaxSupportedNestingDepth() {
+ registry.registerCustomEditor(null, "list", this.editor);
+
+ String propertyPath = "list" + "[0]".repeat(MAX_DEPTH + 1);
+ assertThat(registry.findCustomEditor(null, propertyPath)).isNull();
+ }
+
+ @Test // gh-37020
+ @Timeout(5)
+ void findCustomEditorWithExcessivelyNestedPropertyPathDoesNotHang() {
+ registry.registerCustomEditor(null, "attrs", this.editor);
+
+ // A property path with a bracket-nesting depth (40) that would previously
+ // have caused addStrippedPropertyPaths() to recursively enumerate 2^40 - 1
+ // stripped path variants. With the depth limit in place, this should return
+ // promptly.
+ String propertyPath = "attrs" + "[k]".repeat(40);
+
+ assertThat(registry.findCustomEditor(null, propertyPath)).isNull();
+ }
+
+ @Test
+ void guessPropertyTypeFromEditorsMatchesStrippedPathAtMaxSupportedNestingDepth() {
+ registry.registerCustomEditor(Integer.class, "list", this.editor);
+
+ String propertyPath = "list" + "[0]".repeat(MAX_DEPTH);
+ assertThat(registry.guessPropertyTypeFromEditors(propertyPath)).isEqualTo(Integer.class);
+ }
+
+ @Test
+ void guessPropertyTypeFromEditorsDoesNotMatchStrippedPathBeyondMaxSupportedNestingDepth() {
+ registry.registerCustomEditor(Integer.class, "list", this.editor);
+
+ String propertyPath = "list" + "[0]".repeat(MAX_DEPTH + 1);
+ assertThat(registry.guessPropertyTypeFromEditors(propertyPath)).isNull();
+ }
+
+}