Check list index after auto-grow in AbstractNestablePropertyAccessor

Prior to this commit, the List branch in
AbstractNestablePropertyAccessor's getPropertyValue() method called
list.get(index) unconditionally after invoking
growCollectionIfNecessary(), which implicitly relied on the list
throwing an IndexOutOfBoundsException for out-of-range access. Such an
exception is caught downstream and wrapped as an
InvalidPropertyException; however, any List implementation whose get()
method allocates elements on demand rather than throwing an
IndexOutOfBoundsException could bypass that check.

This behavior was also inconsistent with the Collection/Iterable branch
in the same method, which already performs an explicit `index >=
collection.size()` bounds check before attempting element access.

To address that, this commit aligns the List branch with the
Collection/Iterable branch by adding an explicit `index < 0 || index >=
list.size()` check immediately after the auto-grow attempt. If the
index remains out of bounds after growCollectionIfNecessary() runs –
for example, because growth was capped by autoGrowCollectionLimit or
auto-growing was disabled – an InvalidPropertyException is now thrown
rather than delegating to list.get() which may or may not throw an
exception.

Closes gh-37036
This commit is contained in:
Sam Brannen
2026-08-14 09:11:50 +02:00
committed by Brian Clozel
parent ac0f8be0d8
commit d186b381b9
2 changed files with 54 additions and 1 deletions
@@ -649,6 +649,11 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
else if (value instanceof List list) {
int index = Integer.parseInt(key);
growCollectionIfNecessary(list, index, indexedPropertyName.toString(), ph, i + 1);
if (index < 0 || index >= list.size()) {
throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
"Cannot get element with index " + index + " from List of size " +
list.size() + ", accessed using property path '" + propertyName + "'");
}
value = list.get(index);
}
else if (value instanceof Map map) {
@@ -16,6 +16,7 @@
package org.springframework.beans;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -183,7 +184,34 @@ class BeanWrapperAutoGrowingTests {
wrapper.setAutoGrowCollectionLimit(2);
assertThatExceptionOfType(InvalidPropertyException.class)
.isThrownBy(() -> wrapper.getPropertyValue("list[4]"))
.withRootCauseInstanceOf(IndexOutOfBoundsException.class);
.withMessageContainingAll(
"Invalid property 'list[4]'",
"Cannot get element with index 4 from List of size 0");
}
@Test
void getPropertyValueSelfPopulatingListWorksWithinLimit() {
bean.setList(new SelfPopulatingList());
assertThat(wrapper.getPropertyValue("list[2]")).isInstanceOf(Bean.class);
assertThat(bean.getList())
.hasSize(3)
.allSatisfy(entry -> assertThat(entry).isInstanceOf(Bean.class));
}
@Test
void getPropertyValueSelfPopulatingListFailsAgainstLimit() {
bean.setList(new SelfPopulatingList());
wrapper.setAutoGrowCollectionLimit(2);
assertThatExceptionOfType(InvalidPropertyException.class)
.isThrownBy(() -> wrapper.getPropertyValue("list[4]"));
}
@Test
void setPropertyValueSelfPopulatingListFailsAgainstLimitForNestedPath() {
bean.setList(new SelfPopulatingList());
wrapper.setAutoGrowCollectionLimit(2);
assertThatExceptionOfType(InvalidPropertyException.class)
.isThrownBy(() -> wrapper.setPropertyValue("list[4].prop", "test"));
}
@Test
@@ -382,4 +410,24 @@ class BeanWrapperAutoGrowingTests {
}
}
/**
* A {@link List} implementation that creates elements on demand in {@link #get(int)}
* instead of throwing {@link IndexOutOfBoundsException} for out-of-range indexes.
*
* <p>Used to verify that {@link BeanWrapperImpl} does not delegate to
* {@link List#get(int)} for indexes beyond the configured auto-grow limit.
*/
@SuppressWarnings("serial")
private static class SelfPopulatingList extends ArrayList<Bean> {
@Override
public Bean get(int index) {
while (size() <= index) {
add(new Bean());
}
return super.get(index);
}
}
}