{"id":"GHSA-88fw-v6x4-3f58","summary":"Spring Data: Unbounded property-path cache keyed by externally-supplied path string","details":"`src/main/java/org/springframework/data/mapping/context/PersistentPropertyPathFactory.java:175` · Unbounded Resource Allocation (Algorithmic DoS)\n\n### Impact\n\nWhen 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\u003cn\u003e` values and grow the heap until the service OOMs.\n\n### Description\n\n`PersistentPropertyPathFactory.propertyPaths` (line 53) is a `ConcurrentHashMap\u003cTypeAndPath, PathResolution\u003e` 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.\n\nBy contrast, the sibling `SimplePropertyPath.cache` was already hardened to use `ConcurrentReferenceHashMap` (soft refs); this cache was not given the same treatment.\n\n### Exploit scenario\n\nAgainst a Spring Data REST endpoint, an attacker scripts `GET /things?sort=\u003crandom-40-char-string\u003e` 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.\n\n### Preconditions\n\n- A downstream component (Spring Data REST, a store-specific `QueryMapper`, or application code) passes externally-supplied strings to `MappingContext.getPersistentPropertyPath(String, ...)`\n- No upstream rate-limiting or path-string validation\n\n### How to fix\n\nA cache whose key can be derived from external input must be bounded. Replace `propertyPaths` (line 53) with a `ConcurrentLruCache\u003cTypeAndPath, PathResolution\u003e` 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.\n\n### Adversarial verification\n\n**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.\n\n**Code at the line — CONFIRMED**\n- Line 53: `private final Map\u003cTypeAndPath, PathResolution\u003e propertyPaths = new ConcurrentHashMap\u003c\u003e();` — plain CHM, hard refs, no eviction, no size bound.\n- Line 175: `propertyPaths.computeIfAbsent(TypeAndPath.of(type, propertyPath), ...)` — every distinct `(type, string)` pair is inserted.\n- 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).\n\n**Callers within spring-data-commons**\n- `AbstractMappingContext.getPersistentPropertyPath(String, Class\u003c?\u003e)` (line 345) and `(String, TypeInformation\u003c?\u003e)` (line 350) → `persistentPropertyPathFactory.from(type, propertyPath)` → straight into the unbounded cache. No validation, no `PropertyPath.from` gate.\n- 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.\n- 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.\n\n**Protections — NONE on this cache**\nContrast: `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.\n\n**Stress-test**\nIs 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.","aliases":["CVE-2026-41695"],"modified":"2026-08-02T02:59:55.421055602Z","published":"2026-07-31T16:44:15Z","related":["CGA-q3cp-j72m-6jhx"],"database_specific":{"github_reviewed_at":"2026-07-31T16:44:15Z","nvd_published_at":"2026-06-10T00:16:50Z","cwe_ids":["CWE-400"],"severity":"HIGH","github_reviewed":true},"references":[{"type":"WEB","url":"https://github.com/spring-projects/security-advisories/security/advisories/GHSA-88fw-v6x4-3f58"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-41695"},{"type":"WEB","url":"https://github.com/spring-projects/spring-data-commons/commit/96e9475b963218bb702959524187342671f6b080"},{"type":"WEB","url":"https://github.com/spring-projects/spring-data-commons/commit/a4f893b66c18c70f2b22f29a2b55097b73c89941"},{"type":"PACKAGE","url":"https://github.com/spring-projects/security-advisories"},{"type":"WEB","url":"https://github.com/spring-projects/spring-data-commons/releases/tag/3.5.12"},{"type":"WEB","url":"https://github.com/spring-projects/spring-data-commons/releases/tag/4.0.6"},{"type":"WEB","url":"https://spring.io/security/cve-2026-41695"}],"affected":[{"package":{"name":"org.springframework.data:spring-data-commons","ecosystem":"Maven","purl":"pkg:maven/org.springframework.data/spring-data-commons"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"4.0.0"},{"fixed":"4.0.6"}]}],"versions":["4.0.0","4.0.1","4.0.2","4.0.3","4.0.4","4.0.5"],"database_specific":{"last_known_affected_version_range":"\u003c= 4.0.5","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/07/GHSA-88fw-v6x4-3f58/GHSA-88fw-v6x4-3f58.json"}},{"package":{"name":"org.springframework.data:spring-data-commons","ecosystem":"Maven","purl":"pkg:maven/org.springframework.data/spring-data-commons"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"3.5.0"},{"fixed":"3.5.12"}]}],"versions":["3.5.0","3.5.1","3.5.10","3.5.11","3.5.2","3.5.3","3.5.4","3.5.5","3.5.6","3.5.7","3.5.8","3.5.9"],"database_specific":{"last_known_affected_version_range":"\u003c= 3.5.11","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/07/GHSA-88fw-v6x4-3f58/GHSA-88fw-v6x4-3f58.json"}},{"package":{"name":"org.springframework.data:spring-data-commons","ecosystem":"Maven","purl":"pkg:maven/org.springframework.data/spring-data-commons"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"3.4.0"},{"last_affected":"3.4.13"}]}],"versions":["3.4.0","3.4.1","3.4.10","3.4.11","3.4.12","3.4.13","3.4.2","3.4.3","3.4.4","3.4.5","3.4.6","3.4.7","3.4.8","3.4.9"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/07/GHSA-88fw-v6x4-3f58/GHSA-88fw-v6x4-3f58.json"}}],"schema_version":"1.7.5","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H"}]}