This is an automated email from the ASF dual-hosted git repository.
markt-asf pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/main by this push:
new 7fd8c2845e Add a configurable limit for WebDAV XML request bodies
7fd8c2845e is described below
commit 7fd8c2845e38cf7745fb5c15c81806807f39ea39
Author: Mark Thomas <[email protected]>
AuthorDate: Wed Apr 15 11:19:29 2026 +0100
Add a configurable limit for WebDAV XML request bodies
Only applies where the user does not already have write access
to the resource.
---
.../apache/catalina/servlets/WebdavServlet.java | 69 ++++++++++++++-
.../TestWebdavBoundedByteArrayOutputStream.java | 98 ++++++++++++++++++++++
.../catalina/servlets/TestWebdavServlet.java | 83 ++++++++++++++++++
webapps/docs/changelog.xml | 6 ++
4 files changed, 254 insertions(+), 2 deletions(-)
diff --git a/java/org/apache/catalina/servlets/WebdavServlet.java
b/java/org/apache/catalina/servlets/WebdavServlet.java
index adceaef970..a9e3dfc459 100644
--- a/java/org/apache/catalina/servlets/WebdavServlet.java
+++ b/java/org/apache/catalina/servlets/WebdavServlet.java
@@ -175,6 +175,8 @@ import org.xml.sax.SAXException;
* </init-param>
* </pre>
* <p>
+ * By default, WebDAV request bodies for LOCK and PROPFIND are limited to 4096
bytes. To change this limit, set the
+ * <code>maxRequestBodySize</code> <code>init-param</code> for the WebDAV
servlet.
*
* @see <a href="https://tools.ietf.org/html/rfc4918">RFC 4918</a>
*/
@@ -202,6 +204,12 @@ public class WebdavServlet extends DefaultServlet
implements PeriodicEventListen
private static final int MAX_DEPTH = 3;
+ /*
+ * Default max request body size.
+ */
+ private static final int DEFAULT_MAX_REQUEST_BODY_SIZE = 4096;
+
+
/**
* Default namespace.
*/
@@ -215,6 +223,7 @@ public class WebdavServlet extends DefaultServlet
implements PeriodicEventListen
"\n
<D:lockentry><D:lockscope><D:exclusive/></D:lockscope><D:locktype><D:write/></D:locktype></D:lockentry>\n"
+
"
<D:lockentry><D:lockscope><D:shared/></D:lockscope><D:locktype><D:write/></D:locktype></D:lockentry>\n";
+
/**
* Simple date format for the creation date ISO representation (partial).
*/
@@ -227,6 +236,7 @@ public class WebdavServlet extends DefaultServlet
implements PeriodicEventListen
*/
protected static final String LOCK_SCHEME = "urn:uuid:";
+
// ----------------------------------------------------- Instance Variables
/**
@@ -247,6 +257,9 @@ public class WebdavServlet extends DefaultServlet
implements PeriodicEventListen
private int maxDepth = MAX_DEPTH;
+ private int maxRequestBodySize = DEFAULT_MAX_REQUEST_BODY_SIZE;
+
+
/**
* Is access allowed via WebDAV to the special paths (/WEB-INF and
/META-INF)?
*/
@@ -293,6 +306,10 @@ public class WebdavServlet extends DefaultServlet
implements PeriodicEventListen
maxDepth =
Integer.parseInt(getServletConfig().getInitParameter("maxDepth"));
}
+ if (getServletConfig().getInitParameter("maxRequestBodySize") != null)
{
+ maxRequestBodySize =
Integer.parseInt(getServletConfig().getInitParameter("maxRequestBodySize"));
+ }
+
if (getServletConfig().getInitParameter("allowSpecialPaths") != null) {
allowSpecialPaths =
Boolean.parseBoolean(getServletConfig().getInitParameter("allowSpecialPaths"));
}
@@ -829,13 +846,23 @@ public class WebdavServlet extends DefaultServlet
implements PeriodicEventListen
}
}
+ // Short-cut if client provided a content length
+ if (req.getContentLengthLong() > maxRequestBodySize) {
+ resp.sendError(WebdavStatus.SC_REQUEST_TOO_LONG);
+ return;
+ }
+
byte[] body;
- try (InputStream is = req.getInputStream(); ByteArrayOutputStream os =
new ByteArrayOutputStream()) {
+ try (InputStream is = req.getInputStream();
+ BoundedByteArrayOutputStream os = new
BoundedByteArrayOutputStream(maxRequestBodySize)) {
IOTools.flow(is, os);
body = os.toByteArray();
} catch (IOException ioe) {
resp.sendError(WebdavStatus.SC_BAD_REQUEST);
return;
+ } catch (ArrayIndexOutOfBoundsException e) {
+ resp.sendError(WebdavStatus.SC_REQUEST_TOO_LONG);
+ return;
}
if (body.length > 0) {
DocumentBuilder documentBuilder = getDocumentBuilder();
@@ -1389,13 +1416,23 @@ public class WebdavServlet extends DefaultServlet
implements PeriodicEventListen
Node lockInfoNode = null;
+ // Short-cut if client provided a content length
+ if (req.getContentLengthLong() > maxRequestBodySize) {
+ resp.sendError(WebdavStatus.SC_REQUEST_TOO_LONG);
+ return;
+ }
+
byte[] body;
- try (InputStream is = req.getInputStream(); ByteArrayOutputStream os =
new ByteArrayOutputStream()) {
+ try (InputStream is = req.getInputStream();
+ BoundedByteArrayOutputStream os = new
BoundedByteArrayOutputStream(maxRequestBodySize)) {
IOTools.flow(is, os);
body = os.toByteArray();
} catch (IOException ioe) {
resp.sendError(WebdavStatus.SC_BAD_REQUEST);
return;
+ } catch (ArrayIndexOutOfBoundsException e) {
+ resp.sendError(WebdavStatus.SC_REQUEST_TOO_LONG);
+ return;
}
if (body.length > 0) {
DocumentBuilder documentBuilder = getDocumentBuilder();
@@ -3020,6 +3057,34 @@ public class WebdavServlet extends DefaultServlet
implements PeriodicEventListen
}
+ static class BoundedByteArrayOutputStream extends ByteArrayOutputStream {
+
+ private final int sizeLimit;
+ private int size;
+
+ BoundedByteArrayOutputStream(int sizeLimit) {
+ super();
+ this.sizeLimit = sizeLimit;
+ }
+
+ @Override
+ public synchronized void write(int b) {
+ size++;
+ if (size > sizeLimit) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+ super.write(b);
+ }
+
+ @Override
+ public synchronized void write(byte[] b, int off, int len) {
+ size += len;
+ if (size > sizeLimit) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+ super.write(b, off, len);
+ }
+ }
}
diff --git
a/test/org/apache/catalina/servlets/TestWebdavBoundedByteArrayOutputStream.java
b/test/org/apache/catalina/servlets/TestWebdavBoundedByteArrayOutputStream.java
new file mode 100644
index 0000000000..b3648c92f6
--- /dev/null
+++
b/test/org/apache/catalina/servlets/TestWebdavBoundedByteArrayOutputStream.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.servlets;
+
+import java.io.IOException;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.apache.catalina.servlets.WebdavServlet.BoundedByteArrayOutputStream;
+
+public class TestWebdavBoundedByteArrayOutputStream {
+
+ private static final int TEST_LIMIT = 10;
+ private static final byte[] ONE_BYTE_ARRAY = new byte[] { 0 };
+
+
+ @Test
+ public void testWriteByte() {
+ BoundedByteArrayOutputStream bbaos = new
BoundedByteArrayOutputStream(TEST_LIMIT);
+
+ for (int i = 0; i < TEST_LIMIT; i++) {
+ bbaos.write(0);
+ }
+
+ try {
+ bbaos.write(0);
+ Assert.fail("Writing 11th byte failed to trigger error");
+ } catch (ArrayIndexOutOfBoundsException e) {
+ // Pass
+ }
+ }
+
+
+ @Test
+ public void testWriteByteArray() throws IOException {
+ BoundedByteArrayOutputStream bbaos = new
BoundedByteArrayOutputStream(TEST_LIMIT);
+
+ for (int i = 0; i < TEST_LIMIT; i++) {
+ bbaos.write(ONE_BYTE_ARRAY);
+ }
+
+ try {
+ bbaos.write(ONE_BYTE_ARRAY);
+ Assert.fail("Writing 11th byte failed to trigger error");
+ } catch (ArrayIndexOutOfBoundsException e) {
+ // Pass
+ }
+ }
+
+
+ @Test
+ public void testWriteByteSubArray() {
+ BoundedByteArrayOutputStream bbaos = new
BoundedByteArrayOutputStream(TEST_LIMIT);
+
+ for (int i = 0; i < TEST_LIMIT; i++) {
+ bbaos.write(ONE_BYTE_ARRAY, 0, 1);
+ }
+
+ try {
+ bbaos.write(ONE_BYTE_ARRAY, 0, 1);
+ Assert.fail("Writing 11th byte failed to trigger error");
+ } catch (ArrayIndexOutOfBoundsException e) {
+ // Pass
+ }
+ }
+
+
+ @Test
+ public void testWriteBytes() {
+ BoundedByteArrayOutputStream bbaos = new
BoundedByteArrayOutputStream(TEST_LIMIT);
+
+ for (int i = 0; i < TEST_LIMIT; i++) {
+ bbaos.writeBytes(ONE_BYTE_ARRAY);
+ }
+
+ try {
+ bbaos.writeBytes(ONE_BYTE_ARRAY);
+ Assert.fail("Writing 11th byte failed to trigger error");
+ } catch (ArrayIndexOutOfBoundsException e) {
+ // Pass
+ }
+ }
+}
diff --git a/test/org/apache/catalina/servlets/TestWebdavServlet.java
b/test/org/apache/catalina/servlets/TestWebdavServlet.java
index 0e8cec5078..2bc6c74c5b 100644
--- a/test/org/apache/catalina/servlets/TestWebdavServlet.java
+++ b/test/org/apache/catalina/servlets/TestWebdavServlet.java
@@ -75,6 +75,7 @@ public class TestWebdavServlet extends TomcatBaseTest {
// Create a temp webapp that can be safely written to
File tempWebapp = new File(getTemporaryDirectory(),
"webdav-specialpath"+UUID.randomUUID());
+ tempWebapp.deleteOnExit();
Assert.assertTrue("Failed to mkdirs on
"+tempWebapp.getCanonicalPath(),tempWebapp.mkdirs());
Assert.assertTrue(new File(tempWebapp,"WEB-INF").mkdir());
Assert.assertTrue(new File(tempWebapp,"META-INF").mkdir());
@@ -297,6 +298,7 @@ public class TestWebdavServlet extends TomcatBaseTest {
// Create a temp webapp that can be safely written to
File tempWebapp = new File(getTemporaryDirectory(),
"webdav-properties");
+ tempWebapp.deleteOnExit();
Assert.assertTrue(tempWebapp.mkdirs());
Context ctxt = tomcat.addContext("", tempWebapp.getAbsolutePath());
Wrapper webdavServlet = Tomcat.addServlet(ctxt, "webdav", new
WebdavServlet());
@@ -441,6 +443,7 @@ public class TestWebdavServlet extends TomcatBaseTest {
// Create a temp webapp that can be safely written to
File tempWebapp = new File(getTemporaryDirectory(), "webdav-webapp");
+ tempWebapp.deleteOnExit();
Assert.assertTrue(tempWebapp.mkdirs());
Context ctxt = tomcat.addContext("", tempWebapp.getAbsolutePath());
Wrapper webdavServlet = Tomcat.addServlet(ctxt, "webdav", new
WebdavServlet());
@@ -922,6 +925,7 @@ public class TestWebdavServlet extends TomcatBaseTest {
// Create a temp webapp that can be safely written to
File tempWebapp = new File(getTemporaryDirectory(), "webdav-subpath");
+ tempWebapp.deleteOnExit();
File subPath = new File(tempWebapp, "aaa");
Assert.assertTrue(subPath.mkdirs());
@@ -1018,6 +1022,7 @@ public class TestWebdavServlet extends TomcatBaseTest {
// Create a temp webapp that can be safely written to
File tempWebapp = new File(getTemporaryDirectory(), "webdav-lock");
+ tempWebapp.deleteOnExit();
Assert.assertTrue(tempWebapp.mkdirs());
Context ctxt = tomcat.addContext("", tempWebapp.getAbsolutePath());
Wrapper webdavServlet = Tomcat.addServlet(ctxt, "webdav", new
WebdavServlet());
@@ -1405,6 +1410,7 @@ public class TestWebdavServlet extends TomcatBaseTest {
// Create a temp webapp that can be safely written to
File tempWebapp = new File(getTemporaryDirectory(), "webdav-if");
+ tempWebapp.deleteOnExit();
File folder = new File(tempWebapp,
"/myfolder/myfolder2/myfolder4/myfolder5");
Assert.assertTrue(folder.mkdirs());
File file = new File(folder, "myfile.txt");
@@ -1535,6 +1541,7 @@ public class TestWebdavServlet extends TomcatBaseTest {
// Create a temp webapp that can be safely written to
File tempWebapp = new File(getTemporaryDirectory(), "webdav-store");
+ tempWebapp.deleteOnExit();
Assert.assertTrue(tempWebapp.mkdirs());
Context ctxt = tomcat.addContext("", tempWebapp.getAbsolutePath());
Wrapper webdavServlet = Tomcat.addServlet(ctxt, "webdav", new
WebdavServlet());
@@ -1566,6 +1573,82 @@ public class TestWebdavServlet extends TomcatBaseTest {
validateXml(client.getResponseBody());
}
+
+ /*
+ * Only tests LOCK bodies exceeding limit. Other tests cover valid LOCK
bodies.
+ */
+ @Test
+ public void testLockBodyLimit() throws Exception {
+ doTestLimit("LOCK", LOCK_BODY);
+ }
+
+
+ /*
+ * Only tests PROPFIND bodies exceeding limit. Other tests cover valid
PROPFIND bodies.
+ */
+ @Test
+ public void testPropFindBodyLimit() throws Exception {
+ doTestLimit("PROPFIND", PROPFIND_PROP);
+ }
+
+
+ private void doTestLimit(String method, String requestBody) throws
Exception {
+
+ Tomcat tomcat = getTomcatInstance();
+
+ File appDir = new File("test/webapp");
+ Context ctxt = tomcat.addContext("", appDir.getAbsolutePath());
+
+ Wrapper webdavServlet = Tomcat.addServlet(ctxt, "webdav", new
WebdavServlet());
+ webdavServlet.addInitParameter("listings", "true");
+ webdavServlet.addInitParameter("secret", "foo");
+ webdavServlet.addInitParameter("readonly", "false");
+ webdavServlet.addInitParameter("useStrongETags", "true");
+ webdavServlet.addInitParameter("maxRequestBodySize", "10");
+
+ ctxt.addServletMappingDecoded("/*", "webdav");
+ tomcat.start();
+
+ // With content length
+ Client client = new Client();
+ client.setPort(getPort());
+
+ // @formatter:off
+ client.setRequest(new String[] {
+ method + " / HTTP/1.1" + CRLF +
+ "Host: localhost:" + getPort() + CRLF +
+ "Content-Length: " + requestBody.length() + CRLF +
+ "Connection: Close" + CRLF +
+ CRLF +
+ requestBody
+ });
+ // @formatter:on
+ client.connect();
+ client.processRequest(true);
+ Assert.assertEquals(WebdavStatus.SC_REQUEST_TOO_LONG,
client.getStatusCode());
+
+ // Without content length
+ client.reset();
+
+ // @formatter:off
+ client.setRequest(new String[] {
+ method + " / HTTP/1.1" + CRLF +
+ "Host: localhost:" + getPort() + CRLF +
+ "Transfer-Encoding: chunked" + CRLF +
+ "Connection: Close" + CRLF +
+ CRLF +
+ Integer.toHexString(requestBody.length()) + CRLF +
+ requestBody + CRLF +
+ "0" + CRLF +
+ CRLF
+ });
+ // @formatter:on
+ client.connect();
+ client.processRequest(true);
+ Assert.assertEquals(WebdavStatus.SC_REQUEST_TOO_LONG,
client.getStatusCode());
+ }
+
+
public static class CustomPropertyStore implements PropertyStore {
private String propertyName = null;
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 94808f56f9..1553f61943 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -235,6 +235,12 @@
that both Java and Windows are removing / have removed support for
RC4-HMAC. The guide now uses AES256-SHA1. (markt)
</fix>
+ <fix>
+ Add a new initialisation parameter for WebDAV,
+ <code>maxRequestBodySize</code> which limits the size of a WebDAV
+ request body for LOCK and PROPFIND. The default value is 4096 bytes.
+ (markt)
+ </fix>
</changelog>
</subsection>
<subsection name="Coyote">
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]