This is an automated email from the ASF dual-hosted git repository. markt pushed a commit to branch 10.1.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/10.1.x by this push: new 016c7e0c41 Fix BZ 69731 - correct maxParameterCount tracking. 016c7e0c41 is described below commit 016c7e0c41d2282a31db5b6bdb362394a1bb0c99 Author: Mark Thomas <ma...@apache.org> AuthorDate: Tue Jul 1 19:04:18 2025 +0100 Fix BZ 69731 - correct maxParameterCount tracking. Limit was was smaller than intended for multipart uploads with non-file parts when the parts were processed before query string parameters https://bz.apache.org/bugzilla/show_bug.cgi?id=69731 --- java/org/apache/catalina/connector/Request.java | 49 ++++++- .../catalina/valves/TestParameterLimitValve.java | 143 ++++++++++++++++++++- webapps/docs/changelog.xml | 6 + 3 files changed, 192 insertions(+), 6 deletions(-) diff --git a/java/org/apache/catalina/connector/Request.java b/java/org/apache/catalina/connector/Request.java index dfab466afd..cc1306dff0 100644 --- a/java/org/apache/catalina/connector/Request.java +++ b/java/org/apache/catalina/connector/Request.java @@ -111,6 +111,7 @@ import org.apache.tomcat.util.http.ServerCookies; import org.apache.tomcat.util.http.fileupload.FileItem; import org.apache.tomcat.util.http.fileupload.FileUpload; import org.apache.tomcat.util.http.fileupload.disk.DiskFileItemFactory; +import org.apache.tomcat.util.http.fileupload.impl.FileCountLimitExceededException; import org.apache.tomcat.util.http.fileupload.impl.InvalidContentTypeException; import org.apache.tomcat.util.http.fileupload.impl.SizeException; import org.apache.tomcat.util.http.fileupload.servlet.ServletRequestContext; @@ -2577,6 +2578,26 @@ public class Request implements HttpServletRequest { } } + /* + * When the request body is multipart/form-data, both the parts and the query string count towards + * maxParameterCount. If parseParts() is called before getParameterXXX() then the parts will be parsed before + * the query string. Otherwise, the query string will be parsed first. + * + * maxParameterCount must be respected regardless of which is parsed first. + * + * maxParameterCount is reset from the Connector at the start of every request. + * + * If parts are parsed first, non-file parts will be added to the parameter map and any files will reduce + * maxParameterCount by 1 so that when the query string is parsed the difference between the size of the + * parameter map and maxParameterCount will be the original maxParameterCount less the number of parts. i.e. the + * maxParameterCount applied to the query string will be the original maxParameterCount less the number of + * parts. + * + * If the query string is parsed first, all parameters will be added to the parameter map and, ignoring + * maxPartCount, the part limit will be set to the original maxParameterCount less the size of the parameter + * map. i.e. the maxParameterCount applied to the parts will be the original maxParameterCount less the number + * of query parameters. + */ Parameters parameters = coyoteRequest.getParameters(); parameters.setLimit(maxParameterCount); @@ -2682,6 +2703,9 @@ public class Request implements HttpServletRequest { // Not possible } parameters.addParameter(name, value); + } else { + // Adjust the limit to account for a file part which is not added to the parameter map. + maxParameterCount--; } } @@ -2689,7 +2713,7 @@ public class Request implements HttpServletRequest { } catch (InvalidContentTypeException e) { parameters.setParseFailedReason(FailReason.INVALID_CONTENT_TYPE); partsParseException = new ServletException(e); - } catch (SizeException e) { + } catch (SizeException | FileCountLimitExceededException e) { parameters.setParseFailedReason(FailReason.POST_TOO_LARGE); checkSwallowInput(); partsParseException = new IllegalStateException(e); @@ -2934,13 +2958,30 @@ public class Request implements HttpServletRequest { parametersParsed = true; + /* + * When the request body is multipart/form-data, both the parts and the query string count towards + * maxParameterCount. If parseParts() is called before getParameterXXX() then the parts will be parsed before + * the query string. Otherwise, the query string will be parsed first. + * + * maxParameterCount must be respected regardless of which is parsed first. + * + * maxParameterCount is reset from the Connector at the start of every request. + * + * If parts are parsed first, non-file parts will be added to the parameter map and any files will reduce + * maxParameterCount by 1 so that when the query string is parsed the difference between the size of the + * parameter map and maxParameterCount will be the original maxParameterCount less the number of parts. i.e. the + * maxParameterCount applied to the query string will be the original maxParameterCount less the number of + * parts. + * + * If the query string is parsed first, all parameters will be added to the parameter map and, ignoring + * maxPartCount, the part limit will be set to the original maxParameterCount less the size of the parameter + * map. i.e. the maxParameterCount applied to the parts will be the original maxParameterCount less the number + * of query parameters. + */ Parameters parameters = coyoteRequest.getParameters(); boolean success = false; try { // Set this every time in case limit has been changed via JMX - if (parts != null && maxParameterCount > 0) { - maxParameterCount -= parts.size(); - } parameters.setLimit(maxParameterCount); // getCharacterEncoding() may have been overridden to search for diff --git a/test/org/apache/catalina/valves/TestParameterLimitValve.java b/test/org/apache/catalina/valves/TestParameterLimitValve.java index 44beddd657..3d7c2f799a 100644 --- a/test/org/apache/catalina/valves/TestParameterLimitValve.java +++ b/test/org/apache/catalina/valves/TestParameterLimitValve.java @@ -480,6 +480,8 @@ public class TestParameterLimitValve extends TomcatBaseTest { w.setMultipartConfigElement(new MultipartConfigElement("")); ctx.addServletMappingDecoded("/upload/*", "multipart"); + addFailedRequestFilter(ctx); + tomcat.start(); // Construct a simple multipart body with two parts @@ -503,11 +505,148 @@ public class TestParameterLimitValve extends TomcatBaseTest { if (okExpected) { Assert.assertEquals(HttpServletResponse.SC_OK, rc); } else { - Assert.assertTrue(Integer.toString(rc), rc == HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + Assert.assertTrue(Integer.toString(rc), + rc == HttpServletResponse.SC_BAD_REQUEST || rc == HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE || + rc == HttpServletResponse.SC_INTERNAL_SERVER_ERROR); } } + @Test + public void testMaxParameterCountLimitExceeded01_02_00_00() throws Exception { + doTestMaxParameterCountLimitExceeded(1, 2, 0, 0, false); + } + + + @Test + public void testMaxParameterCountLimitExceeded01_00_02_00() throws Exception { + doTestMaxParameterCountLimitExceeded(1, 0, 2, 0, false); + } + + + @Test + public void testMaxParameterCountLimitExceeded01_00_00_02() throws Exception { + doTestMaxParameterCountLimitExceeded(1, 0, 0, 2, false); + } + + + @Test + public void testMaxParameterCountLimitExceeded01_01_00_00() throws Exception { + doTestMaxParameterCountLimitExceeded(1, 1, 0, 0, true); + } + + + @Test + public void testMaxParameterCountLimitExceeded01_00_01_00() throws Exception { + doTestMaxParameterCountLimitExceeded(1, 0, 1, 0, true); + } + + + @Test + public void testMaxParameterCountLimitExceeded01_00_00_01() throws Exception { + doTestMaxParameterCountLimitExceeded(1, 0, 0, 1, true); + } + + + @Test + public void testMaxParameterCountLimitExceeded02_01_01_00() throws Exception { + doTestMaxParameterCountLimitExceeded(2, 1, 1, 0, true); + } + + + @Test + public void testMaxParameterCountLimitExceeded02_01_0_01() throws Exception { + doTestMaxParameterCountLimitExceeded(2, 1, 0, 1, true); + } + + + @Test + public void testMaxParameterCountLimitExceeded02_00_01_01() throws Exception { + doTestMaxParameterCountLimitExceeded(2, 0, 1, 1, true); + } + + + @Test + public void testMaxParameterCountLimitExceeded03_01_01_01() throws Exception { + doTestMaxParameterCountLimitExceeded(3, 1, 1, 1, true); + } + + + private void doTestMaxParameterCountLimitExceeded(int maxParameterCount, int textPartCount, int filePartCount, + int queryStringCount, boolean okExpected) throws Exception { + + Tomcat tomcat = getTomcatInstance(); + StandardContext ctx = (StandardContext) getProgrammaticRootContext(); + + ParameterLimitValve parameterLimitValve = new ParameterLimitValve(); + ctx.getPipeline().addValve(parameterLimitValve); + // Only looking to test maxParameterCount + parameterLimitValve.setUrlPatternLimits("/upload/.*=" + Integer.toString(maxParameterCount) + ",-1,-1"); + + Wrapper w = Tomcat.addServlet(ctx, "multipart", new MultipartServlet()); + // Use defaults for Multipart + w.setMultipartConfigElement(new MultipartConfigElement("")); + ctx.addServletMappingDecoded("/upload/*", "multipart"); + + addFailedRequestFilter(ctx); + + tomcat.start(); + + // Construct a simple multi-part body + String boundary = "--simpleBoundary"; + + StringBuilder content = new StringBuilder(); + int part = 1; + + for (int i = 0; i < textPartCount; i++) { + content.append("--").append(boundary).append(CRLF); + content.append("Content-Disposition: form-data; name=\"part").append(part).append("\"").append(CRLF); + content.append(CRLF); + content.append("part value ").append(part).append(CRLF); + part++; + } + + for (int i = 0; i < filePartCount; i++) { + content.append("--").append(boundary).append(CRLF); + content.append("Content-Disposition: form-data; name=\"part").append(part).append("\"; filename=\"part") + .append(part).append("\"").append(CRLF); + content.append("Content-Type: text/plain").append(CRLF); + content.append(CRLF); + content.append("part value ").append(part).append(CRLF); + part++; + } + + content.append("--").append(boundary).append("--").append(CRLF); + + StringBuilder queryString = new StringBuilder(); + for (int i = 0; i < queryStringCount; i++) { + if (i > 0) { + queryString.append("&"); + } + queryString.append("param"); + queryString.append(part); + queryString.append("=value"); + queryString.append(part); + part++; + } + + + Map<String,List<String>> reqHeaders = new HashMap<>(); + reqHeaders.put("Content-Type", List.of("multipart/form-data; boundary=" + boundary)); + reqHeaders.put("Content-Length", List.of(Integer.toString(content.length()))); + + int rc = postUrl(content.toString().getBytes(), "http://localhost:" + getPort() + "/upload/endpoint?" + + queryString.toString(), new ByteChunk(), reqHeaders, null); + + if (okExpected) { + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + } else { + Assert.assertTrue(Integer.toString(rc), + rc == HttpServletResponse.SC_BAD_REQUEST || rc == HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE || + rc == HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } + private static class MultipartServlet extends HttpServlet { private static final long serialVersionUID = 1L; @@ -517,8 +656,8 @@ public class TestParameterLimitValve extends TomcatBaseTest { resp.setContentType("text/plain"); resp.setCharacterEncoding("UTF-8"); PrintWriter pw = resp.getWriter(); - pw.println("Parameters: " + req.getParameterMap().size()); pw.println("Parts: " + req.getParts().size()); + pw.println("Parameters: " + req.getParameterMap().size()); } } diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml index 860cfb793e..1066565486 100644 --- a/webapps/docs/changelog.xml +++ b/webapps/docs/changelog.xml @@ -122,6 +122,12 @@ below the web application root with a path that was terminated with a file separator. (remm/markt) </fix> + <fix> + <bug>69731</bug>: Fix an issue that meant that the value of + <code>maxParameterCount</code> applied was smaller than intended for + multipart uploads with non-file parts when the parts were processed + before query string parameters. (markt) + </fix> </changelog> </subsection> <subsection name="Coyote"> --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org