Skip to content

Commit b3d1c1c

Browse files
committed
Add a configurable limit for WebDAV XML request bodies
Only applies where the user does not already have write access to the resource.
1 parent 918dbff commit b3d1c1c

4 files changed

Lines changed: 254 additions & 2 deletions

File tree

java/org/apache/catalina/servlets/WebdavServlet.java

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,8 @@
174174
* </init-param>
175175
* </pre>
176176
* <p>
177+
* By default, WebDAV request bodies for LOCK and PROPFIND are limited to 4096 bytes. To change this limit, set the
178+
* <code>maxRequestBodySize</code> <code>init-param</code> for the WebDAV servlet.
177179
*
178180
* @see <a href="https://tools.ietf.org/html/rfc4918">RFC 4918</a>
179181
*/
@@ -200,6 +202,12 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
200202
private static final int MAX_DEPTH = 3;
201203

202204

205+
/*
206+
* Default max request body size.
207+
*/
208+
private static final int DEFAULT_MAX_REQUEST_BODY_SIZE = 4096;
209+
210+
203211
/**
204212
* Default namespace.
205213
*/
@@ -213,6 +221,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
213221
"\n <D:lockentry><D:lockscope><D:exclusive/></D:lockscope><D:locktype><D:write/></D:locktype></D:lockentry>\n" +
214222
" <D:lockentry><D:lockscope><D:shared/></D:lockscope><D:locktype><D:write/></D:locktype></D:lockentry>\n";
215223

224+
216225
/**
217226
* Simple date format for the creation date ISO representation (partial).
218227
*/
@@ -225,6 +234,7 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
225234
*/
226235
protected static final String LOCK_SCHEME = "urn:uuid:";
227236

237+
228238
// ----------------------------------------------------- Instance Variables
229239

230240
/**
@@ -245,6 +255,9 @@ public class WebdavServlet extends DefaultServlet implements PeriodicEventListen
245255
private int maxDepth = MAX_DEPTH;
246256

247257

258+
private int maxRequestBodySize = DEFAULT_MAX_REQUEST_BODY_SIZE;
259+
260+
248261
/**
249262
* Is access allowed via WebDAV to the special paths (/WEB-INF and /META-INF)?
250263
*/
@@ -291,6 +304,10 @@ public void init() throws ServletException {
291304
maxDepth = Integer.parseInt(getServletConfig().getInitParameter("maxDepth"));
292305
}
293306

307+
if (getServletConfig().getInitParameter("maxRequestBodySize") != null) {
308+
maxRequestBodySize = Integer.parseInt(getServletConfig().getInitParameter("maxRequestBodySize"));
309+
}
310+
294311
if (getServletConfig().getInitParameter("allowSpecialPaths") != null) {
295312
allowSpecialPaths = Boolean.parseBoolean(getServletConfig().getInitParameter("allowSpecialPaths"));
296313
}
@@ -835,13 +852,23 @@ protected void doPropfind(HttpServletRequest req, HttpServletResponse resp) thro
835852
}
836853
}
837854

855+
// Short-cut if client provided a content length
856+
if (req.getContentLengthLong() > maxRequestBodySize) {
857+
resp.sendError(WebdavStatus.SC_REQUEST_TOO_LONG);
858+
return;
859+
}
860+
838861
byte[] body;
839-
try (InputStream is = req.getInputStream(); ByteArrayOutputStream os = new ByteArrayOutputStream()) {
862+
try (InputStream is = req.getInputStream();
863+
BoundedByteArrayOutputStream os = new BoundedByteArrayOutputStream(maxRequestBodySize)) {
840864
IOTools.flow(is, os);
841865
body = os.toByteArray();
842866
} catch (IOException ioe) {
843867
resp.sendError(WebdavStatus.SC_BAD_REQUEST);
844868
return;
869+
} catch (ArrayIndexOutOfBoundsException e) {
870+
resp.sendError(WebdavStatus.SC_REQUEST_TOO_LONG);
871+
return;
845872
}
846873
if (body.length > 0) {
847874
DocumentBuilder documentBuilder = getDocumentBuilder();
@@ -1395,13 +1422,23 @@ protected void doLock(HttpServletRequest req, HttpServletResponse resp) throws S
13951422

13961423
Node lockInfoNode = null;
13971424

1425+
// Short-cut if client provided a content length
1426+
if (req.getContentLengthLong() > maxRequestBodySize) {
1427+
resp.sendError(WebdavStatus.SC_REQUEST_TOO_LONG);
1428+
return;
1429+
}
1430+
13981431
byte[] body;
1399-
try (InputStream is = req.getInputStream(); ByteArrayOutputStream os = new ByteArrayOutputStream()) {
1432+
try (InputStream is = req.getInputStream();
1433+
BoundedByteArrayOutputStream os = new BoundedByteArrayOutputStream(maxRequestBodySize)) {
14001434
IOTools.flow(is, os);
14011435
body = os.toByteArray();
14021436
} catch (IOException ioe) {
14031437
resp.sendError(WebdavStatus.SC_BAD_REQUEST);
14041438
return;
1439+
} catch (ArrayIndexOutOfBoundsException e) {
1440+
resp.sendError(WebdavStatus.SC_REQUEST_TOO_LONG);
1441+
return;
14051442
}
14061443
if (body.length > 0) {
14071444
DocumentBuilder documentBuilder = getDocumentBuilder();
@@ -3025,6 +3062,34 @@ public void proppatch(String resource, ArrayList<ProppatchOperation> operations)
30253062
}
30263063

30273064

3065+
static class BoundedByteArrayOutputStream extends ByteArrayOutputStream {
3066+
3067+
private final int sizeLimit;
3068+
private int size;
3069+
3070+
BoundedByteArrayOutputStream(int sizeLimit) {
3071+
super();
3072+
this.sizeLimit = sizeLimit;
3073+
}
3074+
3075+
@Override
3076+
public synchronized void write(int b) {
3077+
size++;
3078+
if (size > sizeLimit) {
3079+
throw new ArrayIndexOutOfBoundsException();
3080+
}
3081+
super.write(b);
3082+
}
3083+
3084+
@Override
3085+
public synchronized void write(byte[] b, int off, int len) {
3086+
size += len;
3087+
if (size > sizeLimit) {
3088+
throw new ArrayIndexOutOfBoundsException();
3089+
}
3090+
super.write(b, off, len);
3091+
}
3092+
}
30283093
}
30293094

30303095

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.catalina.servlets;
18+
19+
import java.io.IOException;
20+
21+
import org.junit.Assert;
22+
import org.junit.Test;
23+
24+
import org.apache.catalina.servlets.WebdavServlet.BoundedByteArrayOutputStream;
25+
26+
public class TestWebdavBoundedByteArrayOutputStream {
27+
28+
private static final int TEST_LIMIT = 10;
29+
private static final byte[] ONE_BYTE_ARRAY = new byte[] { 0 };
30+
31+
32+
@Test
33+
public void testWriteByte() {
34+
BoundedByteArrayOutputStream bbaos = new BoundedByteArrayOutputStream(TEST_LIMIT);
35+
36+
for (int i = 0; i < TEST_LIMIT; i++) {
37+
bbaos.write(0);
38+
}
39+
40+
try {
41+
bbaos.write(0);
42+
Assert.fail("Writing 11th byte failed to trigger error");
43+
} catch (ArrayIndexOutOfBoundsException e) {
44+
// Pass
45+
}
46+
}
47+
48+
49+
@Test
50+
public void testWriteByteArray() throws IOException {
51+
BoundedByteArrayOutputStream bbaos = new BoundedByteArrayOutputStream(TEST_LIMIT);
52+
53+
for (int i = 0; i < TEST_LIMIT; i++) {
54+
bbaos.write(ONE_BYTE_ARRAY);
55+
}
56+
57+
try {
58+
bbaos.write(ONE_BYTE_ARRAY);
59+
Assert.fail("Writing 11th byte failed to trigger error");
60+
} catch (ArrayIndexOutOfBoundsException e) {
61+
// Pass
62+
}
63+
}
64+
65+
66+
@Test
67+
public void testWriteByteSubArray() {
68+
BoundedByteArrayOutputStream bbaos = new BoundedByteArrayOutputStream(TEST_LIMIT);
69+
70+
for (int i = 0; i < TEST_LIMIT; i++) {
71+
bbaos.write(ONE_BYTE_ARRAY, 0, 1);
72+
}
73+
74+
try {
75+
bbaos.write(ONE_BYTE_ARRAY, 0, 1);
76+
Assert.fail("Writing 11th byte failed to trigger error");
77+
} catch (ArrayIndexOutOfBoundsException e) {
78+
// Pass
79+
}
80+
}
81+
82+
83+
@Test
84+
public void testWriteBytes() {
85+
BoundedByteArrayOutputStream bbaos = new BoundedByteArrayOutputStream(TEST_LIMIT);
86+
87+
for (int i = 0; i < TEST_LIMIT; i++) {
88+
bbaos.writeBytes(ONE_BYTE_ARRAY);
89+
}
90+
91+
try {
92+
bbaos.writeBytes(ONE_BYTE_ARRAY);
93+
Assert.fail("Writing 11th byte failed to trigger error");
94+
} catch (ArrayIndexOutOfBoundsException e) {
95+
// Pass
96+
}
97+
}
98+
}

test/org/apache/catalina/servlets/TestWebdavServlet.java

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ private void testGetSpecials(boolean allowSpecialPaths, boolean useSubpathWebdav
7575

7676
// Create a temp webapp that can be safely written to
7777
File tempWebapp = new File(getTemporaryDirectory(), "webdav-specialpath"+UUID.randomUUID());
78+
tempWebapp.deleteOnExit();
7879
Assert.assertTrue("Failed to mkdirs on "+tempWebapp.getCanonicalPath(),tempWebapp.mkdirs());
7980
Assert.assertTrue(new File(tempWebapp,"WEB-INF").mkdir());
8081
Assert.assertTrue(new File(tempWebapp,"META-INF").mkdir());
@@ -297,6 +298,7 @@ public void testBasicProperties() throws Exception {
297298

298299
// Create a temp webapp that can be safely written to
299300
File tempWebapp = new File(getTemporaryDirectory(), "webdav-properties");
301+
tempWebapp.deleteOnExit();
300302
Assert.assertTrue(tempWebapp.mkdirs());
301303
Context ctxt = tomcat.addContext("", tempWebapp.getAbsolutePath());
302304
Wrapper webdavServlet = Tomcat.addServlet(ctxt, "webdav", new WebdavServlet());
@@ -441,6 +443,7 @@ public void testBasicOperations() throws Exception {
441443

442444
// Create a temp webapp that can be safely written to
443445
File tempWebapp = new File(getTemporaryDirectory(), "webdav-webapp");
446+
tempWebapp.deleteOnExit();
444447
Assert.assertTrue(tempWebapp.mkdirs());
445448
Context ctxt = tomcat.addContext("", tempWebapp.getAbsolutePath());
446449
Wrapper webdavServlet = Tomcat.addServlet(ctxt, "webdav", new WebdavServlet());
@@ -922,6 +925,7 @@ public void testCopyOutsideSubpath() throws Exception {
922925

923926
// Create a temp webapp that can be safely written to
924927
File tempWebapp = new File(getTemporaryDirectory(), "webdav-subpath");
928+
tempWebapp.deleteOnExit();
925929
File subPath = new File(tempWebapp, "aaa");
926930
Assert.assertTrue(subPath.mkdirs());
927931

@@ -1018,6 +1022,7 @@ public void testSharedLocks() throws Exception {
10181022

10191023
// Create a temp webapp that can be safely written to
10201024
File tempWebapp = new File(getTemporaryDirectory(), "webdav-lock");
1025+
tempWebapp.deleteOnExit();
10211026
Assert.assertTrue(tempWebapp.mkdirs());
10221027
Context ctxt = tomcat.addContext("", tempWebapp.getAbsolutePath());
10231028
Wrapper webdavServlet = Tomcat.addServlet(ctxt, "webdav", new WebdavServlet());
@@ -1405,6 +1410,7 @@ public void testIfHeader() throws Exception {
14051410

14061411
// Create a temp webapp that can be safely written to
14071412
File tempWebapp = new File(getTemporaryDirectory(), "webdav-if");
1413+
tempWebapp.deleteOnExit();
14081414
File folder = new File(tempWebapp, "/myfolder/myfolder2/myfolder4/myfolder5");
14091415
Assert.assertTrue(folder.mkdirs());
14101416
File file = new File(folder, "myfile.txt");
@@ -1535,6 +1541,7 @@ public void testPropertyStore() throws Exception {
15351541

15361542
// Create a temp webapp that can be safely written to
15371543
File tempWebapp = new File(getTemporaryDirectory(), "webdav-store");
1544+
tempWebapp.deleteOnExit();
15381545
Assert.assertTrue(tempWebapp.mkdirs());
15391546
Context ctxt = tomcat.addContext("", tempWebapp.getAbsolutePath());
15401547
Wrapper webdavServlet = Tomcat.addServlet(ctxt, "webdav", new WebdavServlet());
@@ -1566,6 +1573,82 @@ public void testPropertyStore() throws Exception {
15661573
validateXml(client.getResponseBody());
15671574
}
15681575

1576+
1577+
/*
1578+
* Only tests LOCK bodies exceeding limit. Other tests cover valid LOCK bodies.
1579+
*/
1580+
@Test
1581+
public void testLockBodyLimit() throws Exception {
1582+
doTestLimit("LOCK", LOCK_BODY);
1583+
}
1584+
1585+
1586+
/*
1587+
* Only tests PROPFIND bodies exceeding limit. Other tests cover valid PROPFIND bodies.
1588+
*/
1589+
@Test
1590+
public void testPropFindBodyLimit() throws Exception {
1591+
doTestLimit("PROPFIND", PROPFIND_PROP);
1592+
}
1593+
1594+
1595+
private void doTestLimit(String method, String requestBody) throws Exception {
1596+
1597+
Tomcat tomcat = getTomcatInstance();
1598+
1599+
File appDir = new File("test/webapp");
1600+
Context ctxt = tomcat.addContext("", appDir.getAbsolutePath());
1601+
1602+
Wrapper webdavServlet = Tomcat.addServlet(ctxt, "webdav", new WebdavServlet());
1603+
webdavServlet.addInitParameter("listings", "true");
1604+
webdavServlet.addInitParameter("secret", "foo");
1605+
webdavServlet.addInitParameter("readonly", "false");
1606+
webdavServlet.addInitParameter("useStrongETags", "true");
1607+
webdavServlet.addInitParameter("maxRequestBodySize", "10");
1608+
1609+
ctxt.addServletMappingDecoded("/*", "webdav");
1610+
tomcat.start();
1611+
1612+
// With content length
1613+
Client client = new Client();
1614+
client.setPort(getPort());
1615+
1616+
// @formatter:off
1617+
client.setRequest(new String[] {
1618+
method + " / HTTP/1.1" + CRLF +
1619+
"Host: localhost:" + getPort() + CRLF +
1620+
"Content-Length: " + requestBody.length() + CRLF +
1621+
"Connection: Close" + CRLF +
1622+
CRLF +
1623+
requestBody
1624+
});
1625+
// @formatter:on
1626+
client.connect();
1627+
client.processRequest(true);
1628+
Assert.assertEquals(WebdavStatus.SC_REQUEST_TOO_LONG, client.getStatusCode());
1629+
1630+
// Without content length
1631+
client.reset();
1632+
1633+
// @formatter:off
1634+
client.setRequest(new String[] {
1635+
method + " / HTTP/1.1" + CRLF +
1636+
"Host: localhost:" + getPort() + CRLF +
1637+
"Transfer-Encoding: chunked" + CRLF +
1638+
"Connection: Close" + CRLF +
1639+
CRLF +
1640+
Integer.toHexString(requestBody.length()) + CRLF +
1641+
requestBody + CRLF +
1642+
"0" + CRLF +
1643+
CRLF
1644+
});
1645+
// @formatter:on
1646+
client.connect();
1647+
client.processRequest(true);
1648+
Assert.assertEquals(WebdavStatus.SC_REQUEST_TOO_LONG, client.getStatusCode());
1649+
}
1650+
1651+
15691652
public static class CustomPropertyStore implements PropertyStore {
15701653

15711654
private String propertyName = null;

webapps/docs/changelog.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,12 @@
156156
that both Java and Windows are removing / have removed support for
157157
RC4-HMAC. The guide now uses AES256-SHA1. (markt)
158158
</fix>
159+
<fix>
160+
Add a new initialisation parameter for WebDAV,
161+
<code>maxRequestBodySize</code> which limits the size of a WebDAV
162+
request body for LOCK and PROPFIND. The default value is 4096 bytes.
163+
(markt)
164+
</fix>
159165
</changelog>
160166
</subsection>
161167
<subsection name="Coyote">

0 commit comments

Comments
 (0)