src/main/java/org/springframework/data/mapping/context/PersistentPropertyPathFactory.java:175 · Unbounded Resource Allocation (Algorithmic DoS) · confidence 0.65
Impact
When a consuming module routes user-supplied dot-paths (sort parameters, projection paths, PATCH paths) through MappingContext.getPersistentPropertyPath(String, Class), each distinct string — including invalid ones — is cached forever. A remote attacker can send millions of requests with unique ?sort=aaaa<n> values and grow the heap until the service OOMs.
Description
PersistentPropertyPathFactory.propertyPaths (line 53) is a ConcurrentHashMap<TypeAndPath, PathResolution> populated by getPotentiallyCachedPath() (line 174-177) via computeIfAbsent. The key is (TypeInformation, rawPathString). Crucially, unresolvable paths are also cached (as PathResolution.unresolved, line 204) so that the same InvalidPersistentPropertyPath can be re-thrown — meaning every distinct garbage string an attacker sends creates a permanent entry. There is no eviction. This is reachable from AbstractMappingContext.getPersistentPropertyPath(String, Class) (AbstractMappingContext.java:345), which Spring Data REST and several store query mappers call with HTTP-request-derived sort/filter property names.
By contrast, the sibling SimplePropertyPath.cache was already hardened to use ConcurrentReferenceHashMap (soft refs); this cache was not given the same treatment.
Exploit scenario
Against a Spring Data REST endpoint, an attacker scripts GET /things?sort=<random-40-char-string> in a loop. Each request fails fast with InvalidPersistentPropertyPath, but each distinct random string leaves behind a TypeAndPath key, a PathResolution object holding the split segments list, and the source string in the map. After ~10M requests the JVM OOMs.
Preconditions
- A downstream component (Spring Data REST, a store-specific
QueryMapper, or application code) passes externally-supplied strings to MappingContext.getPersistentPropertyPath(String, ...)
- No upstream rate-limiting or path-string validation
How to fix
A cache whose key can be derived from external input must be bounded. Replace propertyPaths (line 53) with a ConcurrentLruCache<TypeAndPath, PathResolution> of fixed capacity, or ConcurrentReferenceHashMap (matching the sibling SimplePropertyPath.cache). Additionally, do not cache PathResolution.unresolved results at all (line 204 in createPersistentPropertyPath) — re-computing a failed lookup is cheap, and caching negative results for arbitrary attacker strings is what makes this exploitable.
Adversarial verification
Verdict: TRUE_POSITIVE (confidence: 7/10) — unbounded hard-ref ConcurrentHashMap keyed by raw path string, caches unresolved entries (line 204), reachable via public MappingContext.getPersistentPropertyPath(String, ...); sibling PropertyPath cache was already converted to soft-refs but this one was missed. -3 confidence because the HTTP-input wiring lives in downstream modules (Spring Data REST / store mappers), not verifiable in this repo.
Code at the line — CONFIRMED
- Line 53:
private final Map<TypeAndPath, PathResolution> propertyPaths = new ConcurrentHashMap<>(); — plain CHM, hard refs, no eviction, no size bound.
- Line 175:
propertyPaths.computeIfAbsent(TypeAndPath.of(type, propertyPath), ...) — every distinct (type, string) pair is inserted.
- Line 204:
return PathResolution.unresolved(parts, segment, type, currentPath); — returned from inside computeIfAbsent's mapping function, so unresolvable paths are cached. The PathResolution retains the full attacker string (source = StringUtils.collectionToDelimitedString(parts, "."), line 452).
Callers within spring-data-commons
AbstractMappingContext.getPersistentPropertyPath(String, Class<?>) (line 345) and (String, TypeInformation<?>) (line 350) → persistentPropertyPathFactory.from(type, propertyPath) → straight into the unbounded cache. No validation, no PropertyPath.from gate.
- This is the public
MappingContext interface (MappingContext.java:174,186), documented to throw InvalidPersistentPropertyPath on bad input — i.e., the contract explicitly anticipates being called with possibly-invalid strings.
- No in-repo caller routes HTTP input directly into the
String overload. The Sort.Order.property → getPersistentPropertyPath(String, ...) bridge lives in store modules (Spring Data MongoDB QueryMapper, Spring Data REST sort translator). That wiring is out-of-repo as the finding states.
Protections — NONE on this cache
Contrast: SimplePropertyPath.java:52 (the PropertyPath.from cache) uses ConcurrentReferenceHashMap (soft refs, GC-evictable). Spring already hardened the sibling cache against exactly this pattern. PersistentPropertyPathFactory.propertyPaths was not given the same treatment.
Stress-test
Is this exclusion #3 (intended design)? The PathResolution javadoc (line 426-430) says caching unresolved paths is deliberate — to make repeated lookups of the same bad path cheap. But unbounded hard-ref caching of arbitrary attacker-chosen keys is not the intent; the sibling fix proves Spring considers this a bug class.
src/main/java/org/springframework/data/mapping/context/PersistentPropertyPathFactory.java:175· Unbounded Resource Allocation (Algorithmic DoS) · confidence 0.65Impact
When a consuming module routes user-supplied dot-paths (sort parameters, projection paths, PATCH paths) through
MappingContext.getPersistentPropertyPath(String, Class), each distinct string — including invalid ones — is cached forever. A remote attacker can send millions of requests with unique?sort=aaaa<n>values and grow the heap until the service OOMs.Description
PersistentPropertyPathFactory.propertyPaths(line 53) is aConcurrentHashMap<TypeAndPath, PathResolution>populated bygetPotentiallyCachedPath()(line 174-177) viacomputeIfAbsent. The key is(TypeInformation, rawPathString). Crucially, unresolvable paths are also cached (asPathResolution.unresolved, line 204) so that the sameInvalidPersistentPropertyPathcan be re-thrown — meaning every distinct garbage string an attacker sends creates a permanent entry. There is no eviction. This is reachable fromAbstractMappingContext.getPersistentPropertyPath(String, Class)(AbstractMappingContext.java:345), which Spring Data REST and several store query mappers call with HTTP-request-derived sort/filter property names.By contrast, the sibling
SimplePropertyPath.cachewas already hardened to useConcurrentReferenceHashMap(soft refs); this cache was not given the same treatment.Exploit scenario
Against a Spring Data REST endpoint, an attacker scripts
GET /things?sort=<random-40-char-string>in a loop. Each request fails fast withInvalidPersistentPropertyPath, but each distinct random string leaves behind aTypeAndPathkey, aPathResolutionobject holding the split segments list, and the source string in the map. After ~10M requests the JVM OOMs.Preconditions
QueryMapper, or application code) passes externally-supplied strings toMappingContext.getPersistentPropertyPath(String, ...)How to fix
A cache whose key can be derived from external input must be bounded. Replace
propertyPaths(line 53) with aConcurrentLruCache<TypeAndPath, PathResolution>of fixed capacity, orConcurrentReferenceHashMap(matching the siblingSimplePropertyPath.cache). Additionally, do not cachePathResolution.unresolvedresults at all (line 204 increatePersistentPropertyPath) — re-computing a failed lookup is cheap, and caching negative results for arbitrary attacker strings is what makes this exploitable.Adversarial verification
Verdict: TRUE_POSITIVE (confidence: 7/10) — unbounded hard-ref
ConcurrentHashMapkeyed by raw path string, caches unresolved entries (line 204), reachable via publicMappingContext.getPersistentPropertyPath(String, ...); siblingPropertyPathcache was already converted to soft-refs but this one was missed. -3 confidence because the HTTP-input wiring lives in downstream modules (Spring Data REST / store mappers), not verifiable in this repo.Code at the line — CONFIRMED
private final Map<TypeAndPath, PathResolution> propertyPaths = new ConcurrentHashMap<>();— plain CHM, hard refs, no eviction, no size bound.propertyPaths.computeIfAbsent(TypeAndPath.of(type, propertyPath), ...)— every distinct(type, string)pair is inserted.return PathResolution.unresolved(parts, segment, type, currentPath);— returned from insidecomputeIfAbsent's mapping function, so unresolvable paths are cached. ThePathResolutionretains the full attacker string (source = StringUtils.collectionToDelimitedString(parts, "."), line 452).Callers within spring-data-commons
AbstractMappingContext.getPersistentPropertyPath(String, Class<?>)(line 345) and(String, TypeInformation<?>)(line 350) →persistentPropertyPathFactory.from(type, propertyPath)→ straight into the unbounded cache. No validation, noPropertyPath.fromgate.MappingContextinterface (MappingContext.java:174,186), documented to throwInvalidPersistentPropertyPathon bad input — i.e., the contract explicitly anticipates being called with possibly-invalid strings.Stringoverload. TheSort.Order.property→getPersistentPropertyPath(String, ...)bridge lives in store modules (Spring Data MongoDBQueryMapper, Spring Data REST sort translator). That wiring is out-of-repo as the finding states.Protections — NONE on this cache
Contrast:
SimplePropertyPath.java:52(thePropertyPath.fromcache) usesConcurrentReferenceHashMap(soft refs, GC-evictable). Spring already hardened the sibling cache against exactly this pattern.PersistentPropertyPathFactory.propertyPathswas not given the same treatment.Stress-test
Is this exclusion #3 (intended design)? The
PathResolutionjavadoc (line 426-430) says caching unresolved paths is deliberate — to make repeated lookups of the same bad path cheap. But unbounded hard-ref caching of arbitrary attacker-chosen keys is not the intent; the sibling fix proves Spring considers this a bug class.