diff --git a/commons-fileupload2-core/src/main/java/org/apache/commons/fileupload2/core/AbstractFileUpload.java b/commons-fileupload2-core/src/main/java/org/apache/commons/fileupload2/core/AbstractFileUpload.java index e0c1f503f..6178889b4 100644 --- a/commons-fileupload2-core/src/main/java/org/apache/commons/fileupload2/core/AbstractFileUpload.java +++ b/commons-fileupload2-core/src/main/java/org/apache/commons/fileupload2/core/AbstractFileUpload.java @@ -139,6 +139,16 @@ public static final boolean isMultipartContent(final RequestContext ctx) { */ private int maxPartHeaderSize = MultipartInput.DEFAULT_PART_HEADER_SIZE_MAX; + /** + * The maximum size of the all parts headers in bytes that may be uploaded in a single request. A value of -1 indicates no maximum. + */ + private long partHeaderTotalSizeMax = -1; + + /** + * The size of all headers of all parts that have been read. + */ + private long partHeaderTotalReads; + /** * The content encoding to use when reading part headers. */ @@ -342,6 +352,7 @@ public long getMaxSize() { */ public FileItemHeaders getParsedHeaders(final String headerPart) { final var len = headerPart.length(); + partHeaderTotalReads += len; final var headers = newFileItemHeaders(); var start = 0; for (;;) { @@ -488,6 +499,12 @@ public List parseRequest(final RequestContext requestContext) throws FileUplo String.format("Request '%s' failed: Maximum file count %,d exceeded.", MULTIPART_FORM_DATA, Long.valueOf(maxFileCount)), getMaxFileCount(), size); } + if (partHeaderTotalSizeMax > -1 && partHeaderTotalReads >= partHeaderTotalSizeMax) { + // The next item will exceed total headers size of all parts + throw new FileUploadSizeException( + "The request was rejected because total header size of all parts exceeds the configured partHeaderTotalSizeMax (%s) bytes", + partHeaderTotalSizeMax, partHeaderTotalReads); + } // Don't use getName() here to prevent an InvalidFileNameException. // @formatter:off final var fileItem = fileItemFactory.fileItemBuilder() @@ -575,6 +592,17 @@ public void setMaxPartHeaderSize(final int partHeaderSizeMax) { this.maxPartHeaderSize = partHeaderSizeMax; } + /** + * Sets the total size limit for the all parts headers + * + * @param partHeaderTotalSizeMax The maximum size of the all parts headers in + * bytes that may be uploaded in a single request. + * + */ + public void setPartHeaderTotalSizeMax(long partHeaderTotalSizeMax) { + this.partHeaderTotalSizeMax = partHeaderTotalSizeMax; + } + /** * Sets the maximum allowed size of a complete request, as opposed to {@link #setMaxFileSize(long)}. * diff --git a/commons-fileupload2-core/src/test/java/org/apache/commons/fileupload2/core/AbstractFileUploadTest.java b/commons-fileupload2-core/src/test/java/org/apache/commons/fileupload2/core/AbstractFileUploadTest.java index 1c48a21e9..1c8396d95 100644 --- a/commons-fileupload2-core/src/test/java/org/apache/commons/fileupload2/core/AbstractFileUploadTest.java +++ b/commons-fileupload2-core/src/test/java/org/apache/commons/fileupload2/core/AbstractFileUploadTest.java @@ -21,6 +21,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -478,4 +479,72 @@ void testMultipleRelated() throws Exception { assertEquals("text/plain", part2.getContentType()); assertNull(part2.getName()); } + + + @Test + public void testExceedTotalPartHeaderSizeLimit() throws IOException { + long max = 250; + upload.setPartHeaderTotalSizeMax(max); + try { + // @formatter:off + final var fileItems = parseUpload(upload, + "-----1234\r\n" + + "Content-Disposition: " + + "form-data; name=\"file\"; filename=\"foo.tab\"\r\n" + + "Content-Type: text/whatever\r\n" + + "\r\n" + + "This is the content of the file\n" + + "\r\n" + + "-----1234\r\n" + + "Content-Disposition: form-data; name=\"field\"\r\n" + + "\r\n" + + "fieldValue\r\n" + + "-----1234\r\n" + + "Content-Disposition: form-data; name=\"multi\"\r\n" + + "\r\n" + + "value1\r\n" + + "-----1234\r\n" + + "Content-Disposition: form-data; name=\"multi\"\r\n" + + "Content-Type: text/plain\r\n" + + "Content-ID: multi-id\r\n" + + "\r\n" + + "value2\r\n" + + "-----1234--\r\n"); + // @formatter:on + fail("FileUploadSizeException expected!"); + } catch (FileUploadSizeException fse) { + assertEquals(max, fse.getPermitted()); + } + } + + @Test + public void testPassTotalPartHeaderSizeLimit() throws IOException { + upload.setPartHeaderTotalSizeMax(1 << 10); + // @formatter:off + final var fileItems = parseUpload(upload, + "-----1234\r\n" + + "Content-Disposition: " + + "form-data; name=\"file\"; filename=\"foo.tab\"\r\n" + + "Content-Type: text/whatever\r\n" + + "\r\n" + + "This is the content of the file\n" + + "\r\n" + + "-----1234\r\n" + + "Content-Disposition: form-data; name=\"field\"\r\n" + + "\r\n" + + "fieldValue\r\n" + + "-----1234\r\n" + + "Content-Disposition: form-data; name=\"multi\"\r\n" + + "\r\n" + + "value1\r\n" + + "-----1234\r\n" + + "Content-Disposition: form-data; name=\"multi\"\r\n" + + "Content-Type: text/plain\r\n" + + "Content-ID: multi-id\r\n" + + "\r\n" + + "value2\r\n" + + "-----1234--\r\n"); + // @formatter:on + assertEquals(4, fileItems.size()); + } }