CVE-2026-73635
This Vulnerability has been fixed in the Never-Ending Support (NES) version offered by HeroDevs.
Overview
Apache Struts is a popular open-source web application framework for developing Java EE web applications. Struts resolves user-facing messages such as type-conversion and validation errors through a localized text provider, which caches resolved resource bundles, message formats, and bundle lookup misses per locale. When no fixed locale is configured, the locale used for those lookups comes from the incoming HTTP request.
A Denial of Service (DoS) vulnerability (CVE-2026-73635) has been identified in the localized text provider, which allows unauthenticated remote attackers to grow the framework's internal localized-text caches without bound and exhaust the Java heap, denying service to other users.
Per OWASP: The Denial of Service (DoS) attack is focused on making a resource (site, application, server) unavailable for the purpose it was designed. There are many ways to make a service unavailable for legitimate users by manipulating network packets, programming, logical, or resources handling vulnerabilities, among others. If a service receives a very large number of requests, it may cease to be available to legitimate users. In the same way, a service may stop if a programming vulnerability is exploited, or the way the service handles resources it uses.
This issue affects the internationalization and localized text lookup components of Apache Struts.
Details
Module Info
- Product: Apache Struts
- Affected packages:
org.apache.struts:struts2-core - Affected versions: >=2.0.0 <2.3.38, >=2.5.0 <2.5.34, >=6.0.0 <6.11.0, >=7.0.0 <7.3.0
- GitHub repository: https://github.com/apache/struts
- Published packages: https://central.sonatype.com/artifact/org.apache.struts/struts2-core
- Package manager: Maven
- Fixed in:
- NES for Apache Struts v2.5.40
- OSS Apache Struts 6.11.0 and 7.3.0
Vulnerability Info
This High-severity vulnerability is found in the org.apache.struts:struts2-core package in the internationalization components of Apache Struts.
The localized text provider keeps three caches that are keyed, directly or indirectly, by the locale of the current request. All three are plain concurrent maps with no maximum size and no eviction policy, so every distinct locale that reaches a message lookup adds permanent entries: a resource bundle entry, a message format entry, and, for locales with no matching properties file, a bundle miss entry.
// com/opensymphony/xwork2/util/AbstractLocalizedTextProvider.java
protected final ConcurrentMap<String, ResourceBundle> bundlesMap = new ConcurrentHashMap<>();
private final ConcurrentMap<MessageFormatKey, MessageFormat> messageFormats = new ConcurrentHashMap<>();
private final ConcurrentMap<String, Boolean> missingBundles = new ConcurrentHashMap<>();
private String createMissesKey(String prefix, String aBundleName, Locale locale) {
return prefix + aBundleName + "_" + locale.toString();
}
@Override
public ResourceBundle findResourceBundle(String aBundleName, Locale locale) {
ClassLoader classLoader = getCurrentThreadContextClassLoader();
String key = createMissesKey(String.valueOf(classLoader.hashCode()), aBundleName, locale);
if (missingBundles.containsKey(key)) {
return null;
}
ResourceBundle bundle = null;
try {
if (bundlesMap.containsKey(key)) {
bundle = bundlesMap.get(key);
} else {
bundle = ResourceBundle.getBundle(aBundleName, locale, classLoader);
bundlesMap.putIfAbsent(key, bundle);
}
} catch (MissingResourceException ex) {
// ...
missingBundles.putIfAbsent(key, Boolean.TRUE);
}
return bundle;
}
protected MessageFormat buildMessageFormat(String pattern, Locale locale) {
MessageFormatKey key = new MessageFormatKey(pattern, locale);
MessageFormat format = messageFormats.get(key);
if (format == null) {
format = new MessageFormat(pattern);
format.setLocale(locale);
format.applyPattern(pattern);
messageFormats.put(key, format);
}
return format;
}
The locale that feeds those caches is attacker-controlled under the default configuration. When the struts.locale setting is not defined, the dispatcher takes the locale straight from the request, so the value of an Accept-Language header becomes a cache key without any validation against the locales the runtime actually knows about.
// org/apache/struts2/dispatcher/Dispatcher.java
protected Locale getLocale(HttpServletRequest request) {
Locale locale;
if (defaultLocale != null) {
// struts.locale is configured, parse it
} else {
try {
locale = request.getLocale();
} catch (RuntimeException rex) {
LOG.warn("Cannot get locale from HTTP Request, falling back to system default locale", rex);
locale = Locale.getDefault();
}
}
return locale;
}
Because language tags accept a large space of language, region, and variant combinations, an unauthenticated client can issue a stream of requests that each carry a new Accept-Language value and each trigger a message lookup, for example by submitting input that fails type conversion or validation. Every new locale allocates additional bundle, message format, and bundle miss entries that are never released, so heap use rises monotonically until the JVM throws OutOfMemoryError and stops serving legitimate users. Applications that pin a fixed locale, so that the request-provided locale never reaches a message lookup, are not affected.
This vulnerability was introduced in 2006 with Apache Struts 2.0.0.
Mitigation
Struts 2.5 is no longer community-supported. The community support version will not receive any updates to address this issue. For more information, see the Apache Struts 2.5.x End-Of-Life (EOL) Announcement.
Users of the affected components should apply one of the following mitigations:
- Upgrade affected applications to a supported version of Apache Struts.
- Configure a fixed locale through the
struts.localesetting so that request-provided locales are never used for localized text lookups. - Leverage a commercial support partner like HeroDevs for post-EOL security support.
Credits
- Kuniyoshi Noguchi (finder)