TL;DR
Apache Struts S2-074 documents a denial-of-service vulnerability in localized-text handling. When an application did not configure a fixed locale, Struts used the locale from the incoming request for lookups such as type-conversion and validation messages. The corresponding caches could grow without bound. An unauthenticated remote client could vary the request locale, exhaust the Java heap, and deny service to other users.
The affected ranges are 2.0.0–2.3.37, 2.5.0–2.5.33, 6.0.0–6.10.0, and
7.0.0–7.2.1. Upgrade to 6.11.0 or 7.3.0 at minimum. Apache's workaround is to
set struts.locale to a fixed supported value.
JVN#08517956 records CVSS v4.0 8.7 and independently confirms the public reporter credit.
The important detection feature is not only request rate. It is the cardinality of request-derived values that create cache entries, correlated with heap growth and garbage-collection pressure.
Why it matters
This issue is a useful example of asymmetric resource consumption. A request can be small and valid-looking while causing the server to retain a new cache entry. Repeating the pattern with distinct values grows process-wide state even when total traffic is well below a conventional volumetric DoS threshold.
The same design risk appears in caches keyed by tenant IDs, model names, tool identifiers, callback IDs, header values, or user-selected locales. Any attacker-influenced key space needs both semantic validation and a hard resource bound. Authentication alone is not a memory-management control; S2-074 is reachable without authentication under the affected configuration.
Trust Boundary
The relevant data flow is:
remote request
-> servlet request locale
-> Struts localized-text lookup
-> process-wide cache key / entry
-> retained Java heapWhen struts.locale is not fixed, the request locale crosses from a per-request,
remote-controlled context into process-wide retained state. The trust boundary
is therefore not just HTTP input parsing. It is the transition from ephemeral
input to a cache whose lifetime and resource cost extend beyond the request.
Apache states that applications with a fixed locale are not affected because
localized-text lookups no longer use the request-provided locale. On typical
servlet stacks the request locale is derived from locale negotiation such as
Accept-Language, but defenders should log the effective locale seen by the
application, rather than assume one particular proxy or container behavior.
Root Cause
Two normal features composed unsafely:
- Struts selected a request-derived locale when no fixed locale was configured.
- Localized-text results were retained in internal caches without a size bound.
The vulnerability arose because an unauthenticated client could drive the cardinality of keys in process-wide caches. Cache growth was not constrained by the number of legitimate locales, a configured maximum, or an eviction policy with a finite capacity.
This is a resource-lifetime bug: validation must consider not only whether one locale value can be parsed, but also how many distinct values an attacker can cause the process to retain over time.
Safe reproducer
Do not test heap exhaustion against a shared or production service. A safe validation can establish the security property without taking a JVM down:
- Build a disposable Struts application in an isolated network and configure a deliberately small JVM heap plus a hard test timeout.
- Add one route that performs a normal localized-text lookup. Do not add an artificial allocation loop.
- Record the Struts version,
struts.localesetting, effective request locale, heap use, GC activity, and response status. - Send a small, fixed corpus of synthetic locale variants at a low rate. Keep the corpus and total request count below an approved lab budget.
- On 6.11.0 or 7.3.0, confirm that memory remains bounded and the application stays responsive. If cache metrics are instrumented, confirm that entries do not exceed the configured maximum.
- Repeat with a fixed
struts.locale; changing the request locale should no longer change the locale used for the lookup.
A non-destructive test harness can use this control logic:
for locale in FINITE_SYNTHETIC_LOCALE_CORPUS:
send_one_request(locale)
stop_if(heap_delta > LAB_SAFETY_BUDGET or service_unhealthy)
assert service_healthy
assert fixed_release_cache_is_boundedDo not interpret a short run with no visible heap increase as proof that an old version is safe. Version and configuration inventory remain the authoritative exposure test. The finite corpus is for validating the fix and telemetry, not for reproducing service failure.
Fix assessment
Apache describes two complementary controls in the fixed releases:
- localized-text caches are bounded, with maximum size configurable through
struts.i18n.cacheMaxSize; - request-derived locales can be restricted to the runtime's available-locale
set through
struts.locale.validateRequestLocale.
Apache notes that the optional request-locale restriction is disabled by default for backward compatibility. The bounded caches are therefore the resource-safety control that must hold even when request-locale validation is not enabled. When a cache reaches its limit, entries are evicted transparently. The same fix is present in 7.3.0 and the 6.x maintenance release 6.11.0.
The documented workaround, setting struts.locale to a supported fixed value,
removes the attacker-controlled locale from localized-text lookups. It is useful
for immediate risk reduction but should not be treated as a substitute for
upgrading unsupported 2.x branches.
A fix review should verify bounded behavior under concurrency, not only the presence of the new configuration keys. The public bulletin describes the behavioral change; this article does not claim an unpublished line-by-line patch history or private test result.
Regression tests
| Test | Expected result |
|---|---|
Fixed struts.locale with varied request locales | The configured locale is used consistently |
| Validation disabled with many finite variants | Cache remains within its configured bound |
struts.locale.validateRequestLocale enabled with an unavailable locale | Safe fallback; no unbounded new key |
| Available locale with validation enabled | Correct localized message is returned |
| Cache reaches its configured maximum | Eviction occurs and service remains healthy |
| Evicted locale is requested again | Correct content is recomputed without stale cross-locale data |
| Concurrent requests use many finite variants | Heap, latency, and GC remain within the lab budget |
| Type-conversion and validation errors trigger lookups | Both paths use the bounded-cache behavior |
Include correctness checks for fallback language and cache eviction. A memory fix that returns the wrong user's locale, retains stale messages, or creates a concurrency race is not a complete fix.
Detection
High-cardinality locale input
Capture the effective locale at the application or trusted edge. The following
SPL assumes that value has been normalized into request_locale. The threshold
of 20 distinct values in five minutes is only an initial hypothesis; tune it to
the application's legitimate traffic and NAT behavior.
index=YOUR_WEB_INDEX request_locale=*
| bin _time span=5m
| stats count as requests dc(request_locale) as distinct_locales
values(user_agent) as user_agents
by _time src_ip
| where distinct_locales > 20
| sort - distinct_localesAlso calculate distinct locales across the whole service. An attacker can distribute requests across source addresses, and a reverse proxy can make many clients share one address. Useful pivots include session identifier, TLS client fingerprint, user agent, route, and edge request ID, where lawfully collected.
JVM correlation
Correlate a rise in locale cardinality with:
- old-generation or total heap occupancy that does not return to baseline;
- increased full-GC count or pause time;
- allocation or promotion pressure;
OutOfMemoryError, health-check failure, container restart, or process exit;- latency increases on requests that trigger localized-text lookups.
Request volume by itself is a weak signal for this class. A low request rate can still be relevant when almost every request contributes a new retained key.
Incident Response
- Rate-limit or filter anomalous locale patterns at a trusted edge while preserving representative request evidence.
- Record the exact Struts version and
struts.localeconfiguration on every instance; do not assume a homogeneous fleet. - Preserve access logs, effective-locale telemetry, GC logs, JVM/container metrics, restart history, and deployment metadata.
- Upgrade to 6.11.0 or 7.3.0. If immediate upgrade is impossible, set a fixed
supported
struts.localeas Apache documents. - Restart affected JVMs in a controlled manner after evidence collection to clear retained state, then confirm traffic is served by fixed or mitigated instances.
- Run the bounded-memory regression test and monitor heap after restoration.
A heap dump can contain credentials and user data and can itself add operational pressure. Collect one only under the organization's forensic and privacy policy, with adequate disk and memory headroom.
Disclosure Timeline
- Researcher report date: not published in S2-074.
- 2026-08-13: the public S2-074 page records this as its last-updated date and credits Kuniyoshi Noguchi (野口 晋義), @KuniNogu as reporter.
- 2026-08-25: JVN published JVN#08517956 with CVSS v4.0 8.7 and the public reporter credit.
- Fixed-release availability: S2-074 identifies 6.11.0 and 7.3.0 as the minimum fixed releases; this article does not infer their private patch or validation dates.
No private triage or coordination date is reconstructed from commit history.