Micronaut has Unbounded `bundleCache` in `ResourceBundleMessageSource` that Allows Memory Exhaustion via `Accept-Language` Header

描述

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:

// 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:

// 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 ResourceBundleMessageSourceresolveBundle(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"); }):

# 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:

// 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.

基本資訊

類型
reviewed
嚴重度
low
GitHub 上的公告
開啟公告 ↗
儲存庫公告
開啟儲存庫公告 ↗
原始碼
瀏覽原始碼 ↗
公開(公告)
2026-05-06 19:57:54 UTC
更新時間
2026-06-30 19:52:49 UTC
GitHub 審核
2026-05-06 19:57:54 UTC
NVD 公開
2026-05-12

EPSS Score

Score Percentile
0.21% 10.98%

CVSS Scores

Base score Version Severity Vector
3.7 3.1
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L 點擊展開
攻擊向量 (AV:N)
可經網際網路或企業內可路由網段從遠端觸達,攻擊者不必在裝置旁邊。
攻擊複雜度 (AC:H)
即使網路可達,也常要卡時間窗、負載或特定版本組合才打得響。
權限需求 (PR:N)
不必事先登入或提權,匿名工作階段也可能成為跳板。
使用者互動 (UI:N)
不必受害者點連結、放行巨集或安裝軟體,攻擊鏈可自動走完。
作用域 (S:U)
破壞局限在脆弱元件原本的安全權限與信任域之內。
機密性影響 (C:N)
幾乎談不上實質的敏感資料外洩。
完整性影響 (I:N)
對紀錄真實性與不可否認性的破壞可忽略。
可用性影響 (A:L)
出現明顯延遲、間歇性故障或局部功能不可用,但可用維運手段緩解。

Identifiers

CWEs

CWE id Name
CWE-400 Uncontrolled Resource Consumption

Credits

  • offset (reporter)
  • jojojo8359 (analyst)
  • smallex (analyst)

Affected packages (3)

Vulnerable version ranges and first patched releases as published by GitHub.

Ecosystem Package Vulnerable range First patched Vulnerable functions
maven io.micronaut:micronaut-inject >= 4.10.0, < 4.10.22 4.10.22
maven io.micronaut:micronaut-inject >= 3.10.0, < 3.10.6 3.10.6
maven io.micronaut:micronaut-inject < 3.8.14 3.8.14

References

cvelogic Threat Intelligence