{"id":"GHSA-3rfq-4wpf-qqw3","summary":"Micronaut has Unbounded `bundleCache` in `ResourceBundleMessageSource` that Allows Memory Exhaustion via `Accept-Language` Header","details":"## Summary\n\n`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.\n\n## Details\n\nThe `bundleCache` is initialized in `inject/src/main/java/io/micronaut/context/i18n/ResourceBundleMessageSource.java` at line 150:\n\n```java\n// ResourceBundleMessageSource.java:139-152\nprotected Map\u003cMessageKey, Optional\u003cString\u003e\u003e buildMessageCache() {\n    return new ConcurrentLinkedHashMap.Builder\u003cMessageKey, Optional\u003cString\u003e\u003e()\n            .maximumWeightedCapacity(100)    // ← BOUNDED ✓\n            .build();\n}\n\nprotected Map\u003cMessageKey, Optional\u003cResourceBundle\u003e\u003e buildBundleCache() {\n    return new ConcurrentHashMap\u003c\u003e(18);      // ← UNBOUNDED ✗\n}\n```\n\nThe `resolveBundle()` method at line 169 inserts into `bundleCache` with no eviction policy:\n\n```java\n// ResourceBundleMessageSource.java:169-185\nprivate Optional\u003cResourceBundle\u003e resolveBundle(Locale locale) {\n    MessageKey key = new MessageKey(locale, baseName);\n    final Optional\u003cResourceBundle\u003e resourceBundle = bundleCache.get(key);\n    if (resourceBundle != null) {\n        return resourceBundle;\n    } else {\n        Optional\u003cResourceBundle\u003e opt;\n        try {\n            opt = Optional.of(ResourceBundle.getBundle(baseName, locale, getClassLoader()));\n        } catch (MissingResourceException e) {\n            opt = Optional.empty();\n        }\n        bundleCache.put(key, opt);    // NO SIZE CHECK — unbounded growth\n        return opt;\n    }\n}\n```\n\nThe attack path requires:\n1. The application registers a `ResourceBundleMessageSource` bean (non-default, requires explicit user configuration).\n2. 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).\n3. Each request uses a unique `Accept-Language` value (e.g., `zz-AA`, `zz-AB`, …).\n4. `DefaultHtmlErrorResponseBodyProvider.error()` calls `messageSource.getMessage(code, locale)` → `CompositeMessageSource` delegates to `ResourceBundleMessageSource` → `resolveBundle(locale)` inserts one entry per unique locale into `bundleCache`.\n\nFor 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.\n\nNote: 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.\n\n## PoC\n\nAgainst a Micronaut application with a `ResourceBundleMessageSource` bean registered (e.g., `@Bean ResourceBundleMessageSource messages() { return new ResourceBundleMessageSource(\"messages\"); }`):\n\n```bash\n# Flood bundleCache with unique locales via HTML error path\nfor i in $(seq 1 100000); do\n  curl -s -o /dev/null \\\n    -H \"Accept: text/html\" \\\n    -H \"Accept-Language: zz-$(printf '%04d' $i)\" \\\n    \"http://localhost:8080/nonexistent-path-$(printf '%06d' $i)\" &\n  [ $((i % 200)) -eq 0 ] && wait\ndone\nwait\n```\n\nEach 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.\n\n## Impact\n\n- Only affects applications that explicitly register a `ResourceBundleMessageSource` bean (not the default configuration).\n- Requires the ability to send HTTP requests with `Accept: text/html` headers and control over the `Accept-Language` value.\n- 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.\n- Partial availability impact (A:L) under sustained attack in long-running services.\n\n## Recommended Fix\n\nApply the same bounded-cache pattern used for the sibling `messageCache`:\n\n```java\n// In ResourceBundleMessageSource.java — replace buildBundleCache()\nprotected Map\u003cMessageKey, Optional\u003cResourceBundle\u003e\u003e buildBundleCache() {\n    return new ConcurrentLinkedHashMap.Builder\u003cMessageKey, Optional\u003cResourceBundle\u003e\u003e()\n            .maximumWeightedCapacity(50)    // small — one entry per (locale, baseName)\n            .build();\n}\n```\n\nThe 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.","aliases":["CVE-2026-44242"],"modified":"2026-05-13T16:47:43.237428Z","published":"2026-05-06T19:57:54Z","database_specific":{"severity":"LOW","github_reviewed_at":"2026-05-06T19:57:54Z","nvd_published_at":"2026-05-12T22:16:35Z","github_reviewed":true,"cwe_ids":["CWE-400"]},"references":[{"type":"WEB","url":"https://github.com/micronaut-projects/micronaut-core/security/advisories/GHSA-3rfq-4wpf-qqw3"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-44242"},{"type":"PACKAGE","url":"https://github.com/micronaut-projects/micronaut-core"},{"type":"WEB","url":"https://github.com/micronaut-projects/micronaut-core/releases/tag/v4.10.22"}],"affected":[{"package":{"name":"io.micronaut:micronaut-inject","ecosystem":"Maven","purl":"pkg:maven/io.micronaut/micronaut-inject"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"4.10.22"}]}],"versions":["1.0.0","1.0.0.RC3","1.0.1","1.0.2","1.0.3","1.0.4","1.0.5","1.1.0","1.1.0.M1","1.1.0.M2","1.1.0.RC1","1.1.0.RC2","1.1.1","1.1.2","1.1.3","1.1.4","1.2.0","1.2.0.RC1","1.2.0.RC2","1.2.1","1.2.10","1.2.11","1.2.2","1.2.3","1.2.4","1.2.5","1.2.6","1.2.7","1.2.8","1.2.9","1.3.0","1.3.0.M1","1.3.0.M2","1.3.0.RC1","1.3.1","1.3.2","1.3.3","1.3.4","1.3.5","1.3.6","1.3.7","2.0.0","2.0.0.M1","2.0.0.M2","2.0.0.M3","2.0.0.RC1","2.0.0.RC2","2.0.1","2.0.2","2.0.3","2.1.0","2.1.1","2.1.2","2.1.3","2.1.4","2.2.0","2.2.1","2.2.2","2.2.3","2.3.0","2.3.1","2.3.2","2.3.3","2.3.4","2.4.0","2.4.1","2.4.2","2.4.3","2.4.4","2.5.0","2.5.1","2.5.10","2.5.11","2.5.12","2.5.13","2.5.2","2.5.3","2.5.4","2.5.5","2.5.6","2.5.7","2.5.8","2.5.9","3.0.0","3.0.0-M1","3.0.0-M2","3.0.0-M3","3.0.0-M4","3.0.0-M5","3.0.0-RC1","3.0.1","3.0.2","3.0.3","3.1.0","3.1.1","3.1.2","3.1.3","3.1.4","3.10.0","3.10.1","3.10.2","3.10.3","3.10.4","3.10.5","3.10.6","3.2.0","3.2.1","3.2.2","3.2.3","3.2.4","3.2.5","3.2.6","3.2.7","3.3.0","3.3.0-M1","3.3.1","3.3.2","3.3.3","3.3.4","3.4.0","3.4.1","3.4.2","3.4.3","3.4.4","3.5.0","3.5.1","3.5.2","3.5.3","3.5.4","3.5.5","3.5.6","3.5.7","3.6.0","3.6.1","3.6.2","3.6.3","3.6.4","3.6.5","3.6.6","3.7.0","3.7.1","3.7.2","3.7.3","3.7.4","3.7.5","3.7.6","3.7.7","3.8.0","3.8.1","3.8.10","3.8.11","3.8.12","3.8.13","3.8.14","3.8.2","3.8.3","3.8.4","3.8.5","3.8.6","3.8.7","3.8.8","3.8.9","3.9.0","3.9.1","3.9.2","3.9.3","3.9.4","3.9.5","3.9.6","3.9.7","4.0.0","4.0.0-M1","4.0.0-M2","4.0.0-M3","4.0.0-M4","4.0.0-M5","4.0.0-M6","4.0.0-M7","4.0.0-RC1","4.0.0-RC2","4.0.0-RC3","4.0.0-RC4","4.0.0-RC5","4.0.1","4.0.2","4.0.3","4.0.4","4.0.5","4.0.6","4.0.7","4.1.0","4.1.1","4.1.10","4.1.11","4.1.12","4.1.2","4.1.3","4.1.4","4.1.5","4.1.6","4.1.7","4.1.8","4.1.9","4.10.0","4.10.1","4.10.10","4.10.11","4.10.12","4.10.13","4.10.14","4.10.15","4.10.16","4.10.17","4.10.18","4.10.19","4.10.2","4.10.20","4.10.21","4.10.3","4.10.4","4.10.5","4.10.6","4.10.7","4.10.8","4.10.9","4.2.0","4.2.1","4.2.2","4.2.3","4.2.4","4.3.0","4.3.1","4.3.10","4.3.11","4.3.12","4.3.13","4.3.14","4.3.15","4.3.16","4.3.17","4.3.2","4.3.3","4.3.4","4.3.5","4.3.6","4.3.7","4.3.8","4.3.9","4.4.0","4.4.1","4.4.10","4.4.2","4.4.3","4.4.4","4.4.5","4.4.6","4.4.7","4.4.8","4.4.9","4.5.0","4.5.1","4.5.2","4.5.3","4.5.4","4.6.0","4.6.1","4.6.2","4.6.3","4.6.4","4.6.5","4.6.6","4.6.7","4.6.8","4.7.0","4.7.1","4.7.10","4.7.11","4.7.12","4.7.13","4.7.14","4.7.15","4.7.16","4.7.17","4.7.18","4.7.19","4.7.2","4.7.20","4.7.21","4.7.22","4.7.23","4.7.24","4.7.25","4.7.26","4.7.27","4.7.3","4.7.4","4.7.5","4.7.6","4.7.7","4.7.8","4.7.9","4.8.0","4.8.1","4.8.10","4.8.11","4.8.12","4.8.13","4.8.14","4.8.15","4.8.16","4.8.17","4.8.18","4.8.19","4.8.2","4.8.3","4.8.4","4.8.5","4.8.6","4.8.7","4.8.8","4.8.9","4.9.0","4.9.1","4.9.10","4.9.11","4.9.12","4.9.2","4.9.3","4.9.4","4.9.5","4.9.6","4.9.7","4.9.8","4.9.9"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/05/GHSA-3rfq-4wpf-qqw3/GHSA-3rfq-4wpf-qqw3.json"}}],"schema_version":"1.7.5","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L"}]}