From 6e9534df4d743776463d61d8362ef42dbfadb730 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 26 Aug 2026 16:36:21 +0200 Subject: [PATCH] Limit bracket depth in PropertyEditorRegistrySupport Previously, PropertyEditorRegistrySupport.addStrippedPropertyPaths() recursively enumerated every combination of stripped/retained [key] segments in a property path, producing 2^n - 1 variants for a path with n bracket pairs. To address that, this commit limits the recursion at a depth of 8, preserving existing behavior for realistic property paths while bounding the work done for paths with an unusually large number of bracket segments. Closes gh-37020 --- .../beans/PropertyEditorRegistrySupport.java | 33 ++++++- .../PropertyEditorRegistrySupportTests.java | 93 +++++++++++++++++++ 2 files changed, 121 insertions(+), 5 deletions(-) create mode 100644 spring-beans/src/test/java/org/springframework/beans/PropertyEditorRegistrySupportTests.java 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(); + } + +}