Apache Struts: DoS via OOM owing to no sanity limit on normal form fields in multipart forms
Description
Allocation of Resources Without Limits or Throttling vulnerability in Apache Software Foundation Apache Struts.This issue affects Apache Struts: through 2.5.30, through 6.1.2.
Upgrade to Struts 2.5.31 or 6.1.2.1 or greater
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Apache Struts versions through 2.5.30 and 6.1.2 are vulnerable to a resource allocation DoS via unthrottled multipart request processing.
Description
CVE-2023-34396 is an Allocation of Resources Without Limits or Throttling vulnerability in Apache Struts. The flaw exists in the handling of multipart requests, where the framework does not impose limits on the resources consumed during parsing. This allows an attacker to exhaust server resources, leading to a denial of service (DoS) condition [1]. The issue affects Struts 2.x through 2.5.30 and Struts 6.x through 6.1.2 [1].
Exploitation
An attacker can exploit this vulnerability by sending a crafted multipart request that triggers excessive resource consumption. No authentication is required for exploitation, making it accessible to unauthenticated remote attackers. The attack surface is the default multipart parser used by Struts, which, without proper throttling, can be forced to allocate unbounded memory or CPU time [2].
Impact
Successful exploitation results in a denial of service, rendering the application unavailable. An attacker can cause high CPU usage, memory exhaustion, or both, depending on the resource allocation limits of the server. The impact is limited to availability, as confidentiality and integrity are not directly affected [1].
Mitigation
Users should upgrade to Apache Struts 2.5.31 or 6.1.2.1 (or later) to remediate this vulnerability. The fix introduces a maximum size limit for multipart requests, preventing resource exhaustion [2][4]. No workarounds are documented; upgrading is the recommended action.
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.apache.struts:struts2-coreMaven | < 2.5.31 | 2.5.31 |
org.apache.struts:struts2-coreMaven | >= 6.0.0, < 6.1.2.1 | 6.1.2.1 |
org.apache.struts:struts-coreMaven | <= 1.3.10 | — |
struts:strutsMaven | <= 1.2.9 | — |
Affected products
4- ghsa-coords3 versionspkg:maven/org.apache.struts/struts2-corepkg:maven/org.apache.struts/struts-corepkg:maven/struts/struts
< 2.5.31+ 2 more
- (no CPE)range: < 2.5.31
- (no CPE)range: <= 1.3.10
- (no CPE)range: <= 1.2.9
- Apache Software Foundation/Apache Strutsv5Range: 0
Patches
19 files changed · +108 −15
core/src/main/java/com/opensymphony/xwork2/ognl/accessor/XWorkListPropertyAccessor.java+5 −0 modified@@ -109,6 +109,11 @@ public Object getProperty(Map context, Object target, Object name) throws OgnlEx if (listSize <= index) { Object result; + if (index > autoGrowCollectionLimit) { + throw new OgnlException("Error auto growing collection size to " + index + " which limited to " + + autoGrowCollectionLimit); + } + for (int i = listSize; i < index; i++) { list.add(null); }
core/src/main/java/org/apache/struts2/config/entities/ConstantConfig.java+10 −0 modified@@ -65,6 +65,7 @@ public class ConstantConfig { private String uiThemeExpansionToken; private Long multipartMaxSize; private Long multipartMaxFiles; + private Long multipartMaxStringLength; private String multipartSaveDir; private Integer multipartBufferSize; private BeanConfig multipartParser; @@ -197,6 +198,7 @@ public Map<String, String> getAllAsStringsMap() { map.put(StrutsConstants.STRUTS_UI_THEME_EXPANSION_TOKEN, uiThemeExpansionToken); map.put(StrutsConstants.STRUTS_MULTIPART_MAXSIZE, Objects.toString(multipartMaxSize, null)); map.put(StrutsConstants.STRUTS_MULTIPART_MAXFILES, Objects.toString(multipartMaxFiles, null)); + map.put(StrutsConstants.STRUTS_MULTIPART_MAX_STRING_LENGTH, Objects.toString(multipartMaxStringLength, null)); map.put(StrutsConstants.STRUTS_MULTIPART_SAVEDIR, multipartSaveDir); map.put(StrutsConstants.STRUTS_MULTIPART_BUFFERSIZE, Objects.toString(multipartBufferSize, null)); map.put(StrutsConstants.STRUTS_MULTIPART_PARSER, beanConfToString(multipartParser)); @@ -590,6 +592,14 @@ public void setMultipartMaxFiles(Long multipartMaxFiles) { this.multipartMaxFiles = multipartMaxFiles; } + public Long getMultipartMaxStringLength() { + return multipartMaxStringLength; + } + + public void setMultipartMaxStringLength(Long multipartMaxStringLength) { + this.multipartMaxStringLength = multipartMaxStringLength; + } + public String getMultipartSaveDir() { return multipartSaveDir; }
core/src/main/java/org/apache/struts2/dispatcher/multipart/AbstractMultiPartRequest.java+10 −0 modified@@ -58,6 +58,11 @@ public abstract class AbstractMultiPartRequest implements MultiPartRequest { */ protected Long maxFiles; + /** + * Specifies the maximum length of a string parameter in a multipart request. + */ + protected Long maxStringLength; + /** * Specifies the buffer size to use during streaming. */ @@ -96,6 +101,11 @@ public void setMaxFiles(String maxFiles) { this.maxFiles = Long.parseLong(maxFiles); } + @Inject(StrutsConstants.STRUTS_MULTIPART_MAX_STRING_LENGTH) + public void setMaxStringLength(String maxStringLength) { + this.maxStringLength = Long.parseLong(maxStringLength); + } + @Inject public void setLocaleProviderFactory(LocaleProviderFactory localeProviderFactory) { defaultLocale = localeProviderFactory.createLocaleProvider().getLocale();
core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequest.java+12 −1 modified@@ -137,8 +137,19 @@ protected void processNormalFormField(FileItem item, String charset) throws Unsu values = new ArrayList<>(); } - if (item.getSize() == 0) { + long size = item.getSize(); + if (size == 0) { values.add(StringUtils.EMPTY); + } else if (size > maxStringLength) { + String errorKey = "struts.messages.upload.error.parameter.too.long"; + LocalizedMessage localizedMessage = new LocalizedMessage(this.getClass(), errorKey, null, + new Object[] { item.getFieldName(), maxStringLength, size }); + + if (!errors.contains(localizedMessage)) { + errors.add(localizedMessage); + } + return; + } else if (charset != null) { values.add(item.getString(charset)); } else {
core/src/main/java/org/apache/struts2/StrutsConstants.java+3 −0 modified@@ -145,6 +145,9 @@ public final class StrutsConstants { /** The maximized number of files allowed to upload */ public static final String STRUTS_MULTIPART_MAXFILES = "struts.multipart.maxFiles"; + /** The maximum length of a string parameter in a multipart request. */ + public static final String STRUTS_MULTIPART_MAX_STRING_LENGTH = "struts.multipart.maxStringLength"; + /** The directory to use for storing uploaded files */ public static final String STRUTS_MULTIPART_SAVEDIR = "struts.multipart.saveDir";
core/src/main/resources/org/apache/struts2/default.properties+1 −0 modified@@ -69,6 +69,7 @@ struts.multipart.parser=jakarta struts.multipart.saveDir= struts.multipart.maxSize=2097152 struts.multipart.maxFiles=256 +struts.multipart.maxStringLength=4096 ### Load custom property files (does not override struts.properties!) # struts.custom.properties=application,org/apache/struts2/extension/custom
core/src/main/resources/org/apache/struts2/struts-messages.properties+1 −0 modified@@ -26,6 +26,7 @@ struts.messages.invalid.content.type=Could not find a Content-Type for {0}. Veri struts.messages.removing.file=Removing file {0} {1} struts.messages.error.uploading=Error uploading: {0} struts.messages.error.file.too.large=File {0} is too large to be uploaded. Maximum allowed size is {4} bytes! +struts.messages.upload.error.parameter.too.long=The request parameter "{0}" was too long. Max length allowed is {1}, but found {2}! struts.messages.error.content.type.not.allowed=Content-Type not allowed: {0} "{1}" "{2}" {3} struts.messages.error.file.extension.not.allowed=File extension not allowed: {0} "{1}" "{2}" {3}
core/src/test/java/com/opensymphony/xwork2/ognl/accessor/XWorkListPropertyAccessorTest.java+9 −7 modified@@ -22,7 +22,7 @@ import com.opensymphony.xwork2.XWorkTestCase; import com.opensymphony.xwork2.util.ListHolder; import com.opensymphony.xwork2.util.ValueStack; -import ognl.ListPropertyAccessor; +import com.opensymphony.xwork2.util.reflection.ReflectionContextState; import ognl.PropertyAccessor; import java.util.ArrayList; @@ -42,11 +42,11 @@ public void testContains() { assertNotNull(listHolder.getLongs()); assertEquals(3, listHolder.getLongs().size()); - assertEquals(new Long(1), (Long) listHolder.getLongs().get(0)); - assertEquals(new Long(2), (Long) listHolder.getLongs().get(1)); - assertEquals(new Long(3), (Long) listHolder.getLongs().get(2)); + assertEquals(new Long(1), listHolder.getLongs().get(0)); + assertEquals(new Long(2), listHolder.getLongs().get(1)); + assertEquals(new Long(3), listHolder.getLongs().get(2)); - assertTrue(((Boolean) vs.findValue("longs.contains(1)")).booleanValue()); + assertTrue((Boolean) vs.findValue("longs.contains(1)")); } public void testCanAccessListSizeProperty() { @@ -60,8 +60,8 @@ public void testCanAccessListSizeProperty() { vs.push(listHolder); - assertEquals(new Integer(myList.size()), vs.findValue("strings.size()")); - assertEquals(new Integer(myList.size()), vs.findValue("strings.size")); + assertEquals(myList.size(), vs.findValue("strings.size()")); + assertEquals(myList.size(), vs.findValue("strings.size")); } public void testAutoGrowthCollectionLimit() { @@ -73,12 +73,14 @@ public void testAutoGrowthCollectionLimit() { listHolder.setStrings(myList); ValueStack vs = ActionContext.getContext().getValueStack(); + ReflectionContextState.setCreatingNullObjects(vs.getContext(), true); vs.push(listHolder); vs.setValue("strings[0]", "a"); vs.setValue("strings[1]", "b"); vs.setValue("strings[2]", "c"); vs.setValue("strings[3]", "d"); + vs.findValue("strings[3]"); assertEquals(3, vs.findValue("strings.size()")); }
core/src/test/java/org/apache/struts2/interceptor/FileUploadInterceptorTest.java+57 −7 modified@@ -235,7 +235,7 @@ public void testInvalidContentTypeMultipartRequest() throws Exception { mai.setInvocationContext(ActionContext.getContext()); ActionContext.getContext().setParameters(HttpParameters.create().build()); - ActionContext.getContext().put(ServletActionContext.HTTP_REQUEST, createMultipartRequest(req, 2000)); + ActionContext.getContext().put(ServletActionContext.HTTP_REQUEST, createMultipartRequest(req, 2000, -1)); interceptor.intercept(mai); @@ -257,7 +257,7 @@ public void testNoContentMultipartRequest() throws Exception { mai.setInvocationContext(ActionContext.getContext()); ActionContext.getContext().setParameters(HttpParameters.create().build()); - ActionContext.getContext().put(ServletActionContext.HTTP_REQUEST, createMultipartRequest(req, 2000)); + ActionContext.getContext().put(ServletActionContext.HTTP_REQUEST, createMultipartRequest(req, 2000, -1)); interceptor.intercept(mai); @@ -288,7 +288,7 @@ public void testSuccessUploadOfATextFileMultipartRequest() throws Exception { mai.setInvocationContext(ActionContext.getContext()); Map<String, Object> param = new HashMap<>(); ActionContext.getContext().setParameters(HttpParameters.create(param).build()); - ActionContext.getContext().put(ServletActionContext.HTTP_REQUEST, createMultipartRequest(req, 2000)); + ActionContext.getContext().put(ServletActionContext.HTTP_REQUEST, createMultipartRequest(req, 2000, -1)); interceptor.intercept(mai); @@ -349,7 +349,7 @@ public void testMultipleAccept() throws Exception { mai.setInvocationContext(ActionContext.getContext()); Map<String, Object> param = new HashMap<String, Object>(); ActionContext.getContext().setParameters(HttpParameters.create(param).build()); - ActionContext.getContext().put(ServletActionContext.HTTP_REQUEST, createMultipartRequest(req, 2000)); + ActionContext.getContext().put(ServletActionContext.HTTP_REQUEST, createMultipartRequest(req, 2000, -1)); interceptor.setAllowedTypes("text/html"); interceptor.intercept(mai); @@ -402,7 +402,7 @@ public void testUnacceptedNumberOfFiles() throws Exception { mai.setInvocationContext(ActionContext.getContext()); Map<String, Object> param = new HashMap<>(); ActionContext.getContext().setParameters(HttpParameters.create(param).build()); - ActionContext.getContext().put(ServletActionContext.HTTP_REQUEST, createMultipartRequest(req, 2000)); + ActionContext.getContext().put(ServletActionContext.HTTP_REQUEST, createMultipartRequest(req, 2000, -1)); interceptor.setAllowedTypes("text/html"); interceptor.intercept(mai); @@ -413,6 +413,55 @@ public void testUnacceptedNumberOfFiles() throws Exception { assertEquals("Request exceeded allowed number of files! Max allowed files number is: 3!", action.getActionErrors().iterator().next()); } + public void testMultipartRequestMaxStringLength() throws Exception { + MockHttpServletRequest req = new MockHttpServletRequest(); + req.setCharacterEncoding(StandardCharsets.UTF_8.name()); + req.setMethod("post"); + req.addHeader("Content-type", "multipart/form-data; boundary=---1234"); + + // inspired by the unit tests for jakarta commons fileupload + String content = ("-----1234\r\n" + + "Content-Disposition: form-data; name=\"file\"; filename=\"deleteme.txt\"\r\n" + + "Content-Type: text/html\r\n" + + "\r\n" + + "Unit test of FileUploadInterceptor" + + "\r\n" + + "-----1234\r\n" + + "Content-Disposition: form-data; name=\"normalFormField1\"\r\n" + + "\r\n" + + "it works" + + "\r\n" + + "-----1234\r\n" + + "Content-Disposition: form-data; name=\"normalFormField2\"\r\n" + + "\r\n" + + "long string should not work" + + "\r\n" + + "-----1234--\r\n"); + req.setContent(content.getBytes(StandardCharsets.US_ASCII)); + + MyFileupAction action = container.inject(MyFileupAction.class); + + MockActionInvocation mai = new MockActionInvocation(); + mai.setAction(action); + mai.setResultCode("success"); + mai.setInvocationContext(ActionContext.getContext()); + Map<String, Object> param = new HashMap<>(); + ActionContext.getContext() + .withParameters(HttpParameters.create(param).build()) + .withServletRequest(createMultipartRequest(req, -1, 20)); + + interceptor.intercept(mai); + + assertTrue(action.hasActionErrors()); + + Collection<String> errors = action.getActionErrors(); + assertEquals(1, errors.size()); + String msg = errors.iterator().next(); + assertEquals( + "The request parameter \"normalFormField2\" was too long. Max length allowed is 20, but found 27!", + msg); + } + public void testMultipartRequestLocalizedError() throws Exception { MockHttpServletRequest req = new MockHttpServletRequest(); req.setCharacterEncoding(StandardCharsets.UTF_8.name()); @@ -439,7 +488,7 @@ public void testMultipartRequestLocalizedError() throws Exception { ActionContext.getContext() .withParameters(HttpParameters.create(param).build()) .withLocale(Locale.GERMAN) - .withServletRequest(createMultipartRequest(req, 10)); + .withServletRequest(createMultipartRequest(req, 10, -1)); interceptor.intercept(mai); @@ -472,10 +521,11 @@ private String encodeTextFile(String bondary, String endline, String name, Strin return sb.toString(); } - private MultiPartRequestWrapper createMultipartRequest(HttpServletRequest req, int maxsize) throws IOException { + private MultiPartRequestWrapper createMultipartRequest(HttpServletRequest req, int maxsize, int maxStringLength) throws IOException { JakartaMultiPartRequest jak = new JakartaMultiPartRequest(); jak.setMaxSize(String.valueOf(maxsize)); jak.setMaxFiles("3"); + jak.setMaxStringLength(String.valueOf(maxStringLength)); return new MultiPartRequestWrapper(jak, req, tempDir.getAbsolutePath(), new DefaultLocaleProvider()); }
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
9- cwiki.apache.org/confluence/display/WW/S2-064ghsavendor-advisoryWEB
- github.com/advisories/GHSA-4g42-gqrg-4633ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-34396ghsaADVISORY
- www.openwall.com/lists/oss-security/2023/06/14/3ghsaWEB
- github.com/apache/struts/commit/2d6f1bc0a6f5ac575a56784ac6461816b67c4f21ghsaWEB
- github.com/apache/struts/releases/tag/STRUTS_2_5_31ghsaWEB
- github.com/apache/struts/releases/tag/STRUTS_6_1_2_1ghsaWEB
- security.netapp.com/advisory/ntap-20230706-0005ghsaWEB
- security.netapp.com/advisory/ntap-20230706-0005/mitre
News mentions
0No linked articles in our index yet.