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
This commit is contained in:
Sam Brannen
2026-08-26 16:36:40 +02:00
parent a00fb1b5ae
commit 6e9534df4d
2 changed files with 121 additions and 5 deletions
@@ -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}.
*
* <p>Provides management of default editors and custom editors.
*
* <p>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.
* <p>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<String> strippedPaths, String nestedPath, String propertyPath) {
addStrippedPropertyPaths(strippedPaths, nestedPath, propertyPath, 0);
}
private void addStrippedPropertyPaths(
List<String> 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);
}
}
}
@@ -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();
}
}