Poison

关于 Tomcat POST 大字符串时可能触发 OOM 的问题

在 Tomcat 的官方配置文档 Apache Tomcat 8 Configuration Reference 中,其中对 maxPostSize 的描述如下:

The maximum size in bytes of the POST which will be handled by the container FORM URL parameter parsing. The limit can be disabled by setting this attribute to a value less than zero. If not specified, this attribute is set to 2097152 (2 megabytes). Note that the FailedRequestFilter can be used to reject requests that exceed this limit.

可见 maxPostSize 默认为 2MB,但是我们发现,在最大堆大小配置较低的应用服务器上,若客户端上传数百兆的大字符串则会触发 OOM 而未进行 maxPostSize 的检查,笔者在本地对该问题进行了复现,我们指定 JVM 最大堆大小为 512MB,使用 HTTP POST 一个文件大小为 100MB 的大字符串,当 Content-Type 指定为 multipart/form-data 时,数据以流的方式传递至服务器作为文件存储,而在检查 maxPostSize 时,Tomcat 的源码中是先将文件的字节流转换为 String 对象后再判断的文件的大小,导致当转换字符串时若峰值内存超过了最大堆限制,则会触发 OOM,以文件大小为 100MB 的字符串来算,假设全为英文或数字,转换为 Java 中 String 底层实现 char[] 后,大概占用的内存为 200MB,因为 Java 中一个 char 占用两个字节,这还不算其他对象头等损耗,且在由字符数组转换 char[] 过程中,字符数组占用的 100MB 内存不能被 GC,导致该两部分就会消耗 300MB 以上的内存,在 Web 应用本身还占用了两三百兆的基础上,直接触发了 OOM。

同时笔者想到该 bug 可能可以被攻击者利用,如果我们构造一个比对方服务器内存可承载的字符串更大的字符串进行 POST 提交,则可以使对方服务器 OOM,最后笔者提交了 PR 对该问题进行了修复。

part.getString will cause OOM without checking maxPostSize, checking maxPostSize first will avoid OOM caused by huge string
Fix #419. Check parameter value size before conversion to String

1
2
3
4
5
6
7
<fix>
<pr>419</pr>: When processing POST requests of type
<code>multipart/form-data</code> for parts without a filename that are
added to the parameter map in String form, check the size of the part
before attempting conversion to String. Pull request provided by
tianshuang. (markt)
</fix>