CVE-2026-44242
LOW3.7EPSS 0.05%Micronaut has Unbounded `bundleCache` in `ResourceBundleMessageSource` that Allows Memory Exhaustion via `Accept-Language` Header
Description
## Summary `ResourceBundleMessageSource` maintains two caches: `messageCache` (bounded at 100 entries via `ConcurrentLinkedHashMap`) and `bundleCache` (unbounded `ConcurrentHashMap`). The `bundleCache` is keyed by `(Locale, baseName)` where the locale originates from the HTTP `Accept-Language` header. In applications that explicitly register a `ResourceBundleMessageSource` bean and serve HTML error responses, an unauthenticated attacker can exhaust heap memory by sending requests with large numbers of unique `Accept-Language` values, each causing a new entry in the unbounded `bundleCache`. Unlike GHSA-2hcp-gjrf-7fhc and the sibling `messageCache` (both bounded), `bundleCache` was not updated to use a bounded cache implementation. ## Details The `bundleCache` is initialized in `inject/src/main/java/io/micronaut/context/i18n/ResourceBundleMessageSource.java` at line 150: ```java // ResourceBundleMessageSource.java:139-152 protected Map<MessageKey, Optional<String>> buildMessageCache() { return new ConcurrentLinkedHashMap.Builder<MessageKey, Optional<String>>() .maximumWeightedCapacity(100) // ← BOUNDED ✓ .build(); } protected Map<MessageKey, Optional<ResourceBundle>> buildBundleCache() { return new ConcurrentHashMap<>(18); // ← UNBOUNDED ✗ } ``` The `resolveBundle()` method at line 169 inserts into `bundleCache` with no eviction policy: ```java // ResourceBundleMessageSource.java:169-185 private Optional<ResourceBundle> resolveBundle(Locale locale) { MessageKey key = new MessageKey(locale, baseName); final Optional<ResourceBundle> resourceBundle = bundleCache.get(key); if (resourceBundle != null) { return resourceBundle; } else { Optional<ResourceBundle> opt; try { opt = Optional.of(ResourceBundle.getBundle(baseName, locale, getClassLoader())); } catch (MissingResourceException e) { opt = Optional.empty(); } bundleCache.put(key, opt); // NO SIZE CHECK — unbounded growth return opt; } } ``` The attack path requires: 1. The application registers a `ResourceBundleMessageSource` bean (non-default, requires explicit user configuration). 2. The attacker sends requests that trigger HTML error responses — i.e., requests with `Accept: text/html` to any URL that returns an error (e.g., 404 for any non-existent path). 3. Each request uses a unique `Accept-Language` value (e.g., `zz-AA`, `zz-AB`, …). 4. `DefaultHtmlErrorResponseBodyProvider.error()` calls `messageSource.getMessage(code, locale)` → `CompositeMessageSource` delegates to `ResourceBundleMessageSource` → `resolveBundle(locale)` inserts one entry per unique locale into `bundleCache`. For locales that don't match any bundle file, `ResourceBundle.getBundle()` throws `MissingResourceException` and `Optional.empty()` is stored — a low-cost sentinel. For locales that DO match a bundle, a full `ResourceBundle` object is retained in memory. In either case, the map itself and the `MessageKey` objects grow without bound. Note: the `messageCache` is bounded at 100 entries but does not prevent `bundleCache` growth, as `resolveBundle()` is called directly (bypassing `messageCache`) whenever a `messageCache` miss occurs. ## PoC Against a Micronaut application with a `ResourceBundleMessageSource` bean registered (e.g., `@Bean ResourceBundleMessageSource messages() { return new ResourceBundleMessageSource("messages"); }`): ```bash # Flood bundleCache with unique locales via HTML error path for i in $(seq 1 100000); do curl -s -o /dev/null \ -H "Accept: text/html" \ -H "Accept-Language: zz-$(printf '%04d' $i)" \ "http://localhost:8080/nonexistent-path-$(printf '%06d' $i)" & [ $((i % 200)) -eq 0 ] && wait done wait ``` Each unique `zz-XXXX` tag creates one new `bundleCache` entry. The `MessageKey` (Locale + baseName) and map overhead cost approximately 100-200 bytes per entry. At 100,000 entries, heap consumption from the cache alone reaches roughly 20 MB — significant in resource-constrained deployments. If a locale matches a bundle file, retained `ResourceBundle` objects cost substantially more per entry. ## Impact - Only affects applications that explicitly register a `ResourceBundleMessageSource` bean (not the default configuration). - Requires the ability to send HTTP requests with `Accept: text/html` headers and control over the `Accept-Language` value. - Memory grows approximately 100-200 bytes per novel locale (for non-matching locales) up to several KB per locale if bundles are found. Sustained attack over time causes gradual heap exhaustion. - Partial availability impact (A:L) under sustained attack in long-running services. ## Recommended Fix Apply the same bounded-cache pattern used for the sibling `messageCache`: ```java // In ResourceBundleMessageSource.java — replace buildBundleCache() protected Map<MessageKey, Optional<ResourceBundle>> buildBundleCache() { return new ConcurrentLinkedHashMap.Builder<MessageKey, Optional<ResourceBundle>>() .maximumWeightedCapacity(50) // small — one entry per (locale, baseName) .build(); } ``` The number of distinct resource bundle files is bounded at compile time; a limit of 50 entries is more than sufficient for any realistic i18n configuration while fully preventing unbounded growth.
Affected packages (1)
- Maven/io.micronaut:micronaut-injectfrom 0, < 4.10.22
CVSS scores
| Source | Version | Severity | Vector |
|---|---|---|---|
| osv | CVSS 3.1 | LOW3.7 | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L |