You are viewing a plain text version of this content. The canonical link for it is here.
Posted to server-dev@james.apache.org by bt...@apache.org on 2026/09/03 04:26:52 UTC
(james-mime4j) 04/04: [FIX] Limit total header size to 1MB
This is an automated email from the ASF dual-hosted git repository.
chibenwa pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-mime4j.git
commit fd7d38b1c9b5ac4b4912ac642b4c508b72e47d82
Author: Benoit TELLIER <bt...@linagora.com>
AuthorDate: Mon Aug 31 16:54:23 2026 +0200
[FIX] Limit total header size to 1MB
---
.../org/apache/james/mime4j/stream/MimeConfig.java | 46 ++++++++++-
.../james/mime4j/stream/MimeTokenStream.java | 35 +++++++-
.../james/mime4j/stream/MimeEntityLimitsTest.java | 95 ++++++++++++++++++++++
.../james/mime4j/dom/LargeMessageParsingTest.java | 39 +++++++--
4 files changed, 205 insertions(+), 10 deletions(-)
diff --git a/core/src/main/java/org/apache/james/mime4j/stream/MimeConfig.java b/core/src/main/java/org/apache/james/mime4j/stream/MimeConfig.java
index e8a55b7f..f6e44b86 100644
--- a/core/src/main/java/org/apache/james/mime4j/stream/MimeConfig.java
+++ b/core/src/main/java/org/apache/james/mime4j/stream/MimeConfig.java
@@ -29,7 +29,7 @@ public final class MimeConfig {
public static final MimeConfig PERMISSIVE = MimeConfig.custom()
.setMaxContentLen(100 * 1024 * 1024)
.setMaxHeaderCount(4096)
- .setMaxHeaderLen(-1)
+ .setMaxHeaderLen(64 * 1024)
.setMaxLineLen(-1)
.build();
public static final MimeConfig DEFAULT = new Builder().build();
@@ -44,6 +44,7 @@ public final class MimeConfig {
private final int maxHeaderLen;
private final long maxContentLen;
private final int maxTotalHeaderCount;
+ private final long maxTotalHeaderLen;
private final int maxPartCount;
private final int maxNestingDepth;
private final boolean countLineNumbers;
@@ -57,6 +58,7 @@ public final class MimeConfig {
int maxHeaderLen,
long maxContentLen,
int maxTotalHeaderCount,
+ long maxTotalHeaderLen,
int maxPartCount,
int maxNestingDepth,
boolean countLineNumbers,
@@ -70,6 +72,7 @@ public final class MimeConfig {
this.maxHeaderLen = maxHeaderLen;
this.maxContentLen = maxContentLen;
this.maxTotalHeaderCount = maxTotalHeaderCount;
+ this.maxTotalHeaderLen = maxTotalHeaderLen;
this.maxPartCount = maxPartCount;
this.maxNestingDepth = maxNestingDepth;
this.headlessParsing = headlessParsing;
@@ -151,6 +154,17 @@ public final class MimeConfig {
return maxTotalHeaderCount;
}
+ /**
+ * Returns the maximum total header length limit
+ *
+ * @see Builder#setMaxTotalHeaderLen(long)
+ *
+ * @return value of the maximum total header length limit
+ */
+ public long getMaxTotalHeaderLen() {
+ return maxTotalHeaderLen;
+ }
+
/**
* Returns the maximum part count limit
*
@@ -202,6 +216,7 @@ public final class MimeConfig {
.append(", maxHeaderLen=").append(maxHeaderLen)
.append(", maxContentLen=").append(maxContentLen)
.append(", maxTotalHeaderCount=").append(maxTotalHeaderCount)
+ .append(", maxTotalHeaderLen=").append(maxTotalHeaderLen)
.append(", maxPartCount=").append(maxPartCount)
.append(", maxNestingDepth=").append(maxNestingDepth)
.append(", countLineNumbers=").append(countLineNumbers)
@@ -226,6 +241,7 @@ public final class MimeConfig {
.setMaxHeaderLen(config.getMaxHeaderLen())
.setMaxContentLen(config.getMaxContentLen())
.setMaxTotalHeaderCount(config.getMaxTotalHeaderCount())
+ .setMaxTotalHeaderLen(config.getMaxTotalHeaderLen())
.setMaxPartCount(config.getMaxPartCount())
.setMaxNestingDepth(config.getMaxNestingDepth())
.setCountLineNumbers(config.isCountLineNumbers())
@@ -241,6 +257,7 @@ public final class MimeConfig {
private int maxHeaderLen;
private long maxContentLen;
private int maxTotalHeaderCount;
+ private long maxTotalHeaderLen;
private int maxPartCount;
private int maxNestingDepth;
private boolean countLineNumbers;
@@ -256,6 +273,7 @@ public final class MimeConfig {
this.maxHeaderLen = 10000;
this.maxContentLen = -1;
this.maxTotalHeaderCount = 16384;
+ this.maxTotalHeaderLen = 1024 * 1024;
this.maxPartCount = 512;
this.maxNestingDepth = 64;
this.headlessParsing = null;
@@ -386,6 +404,31 @@ public final class MimeConfig {
return this;
}
+ /**
+ * Sets the maximum number of bytes a message may spend on header fields in
+ * total, across every entity. Parsing will be terminated with a
+ * {@link org.apache.james.mime4j.io.MaxHeaderLengthLimitException} if a
+ * message carries more header bytes than this limit. If this parameter is
+ * set to a non positive value the total header length check is disabled.
+ * <p>
+ * This is to {@link #setMaxHeaderLen(int)} what
+ * {@link #setMaxTotalHeaderCount(int)} is to {@link #setMaxHeaderCount(int)}.
+ * A per header bound multiplies by the number of entities: {@code Content-Type}
+ * and {@code Content-Disposition} occur once per part, so bounding each one
+ * still leaves {@link #setMaxPartCount(int)} times that much. Address, group
+ * and parameter lists are retained as one object per item, so only a message
+ * wide budget on header bytes bounds the retained graph.
+ * <p>
+ * Default value: <code>1048576</code> (1 MB)
+ *
+ * @param maxTotalHeaderLen
+ * maximum total header length limit
+ */
+ public Builder setMaxTotalHeaderLen(long maxTotalHeaderLen) {
+ this.maxTotalHeaderLen = maxTotalHeaderLen;
+ return this;
+ }
+
/**
* Sets the maximum number of MIME entities (body parts and embedded
* messages) a message may be made of. Parsing will be terminated with a
@@ -471,6 +514,7 @@ public final class MimeConfig {
maxHeaderLen,
maxContentLen,
maxTotalHeaderCount,
+ maxTotalHeaderLen,
maxPartCount,
maxNestingDepth,
countLineNumbers,
diff --git a/core/src/main/java/org/apache/james/mime4j/stream/MimeTokenStream.java b/core/src/main/java/org/apache/james/mime4j/stream/MimeTokenStream.java
index 0c8271ef..1bd46c57 100644
--- a/core/src/main/java/org/apache/james/mime4j/stream/MimeTokenStream.java
+++ b/core/src/main/java/org/apache/james/mime4j/stream/MimeTokenStream.java
@@ -31,9 +31,11 @@ import org.apache.james.mime4j.Charsets;
import org.apache.james.mime4j.MimeException;
import org.apache.james.mime4j.codec.DecodeMonitor;
import org.apache.james.mime4j.io.LineNumberInputStream;
+import org.apache.james.mime4j.io.MaxHeaderLengthLimitException;
import org.apache.james.mime4j.io.MaxHeaderLimitException;
import org.apache.james.mime4j.io.MaxNestingDepthLimitException;
import org.apache.james.mime4j.io.MaxPartCountLimitException;
+import org.apache.james.mime4j.util.ByteSequence;
import org.apache.james.mime4j.util.CharsetUtil;
/**
@@ -93,6 +95,7 @@ public class MimeTokenStream {
private MimeEntity rootentity;
private int partCount;
private int totalHeaderCount;
+ private long totalHeaderLen;
/**
* Constructs a standard (lax) stream.
@@ -211,6 +214,7 @@ public class MimeTokenStream {
currentStateMachine = rootentity;
partCount = 0;
totalHeaderCount = 0;
+ totalHeaderLen = 0;
entities.clear();
entities.add(currentStateMachine);
state = currentStateMachine.getState();
@@ -388,7 +392,7 @@ public class MimeTokenStream {
state = currentStateMachine.getState();
if (state != EntityState.T_END_OF_STREAM) {
if (state == EntityState.T_FIELD) {
- checkTotalHeaderLimit();
+ checkTotalHeaderLimits();
}
return state;
}
@@ -440,16 +444,43 @@ public class MimeTokenStream {
* a consumer building a DOM, so only a message wide budget bounds that graph
* independently of the part count.
* <p>
+ * The same reasoning applies to header bytes: {@link MimeConfig#getMaxHeaderLen()}
+ * bounds one field, but {@code Content-Type} and {@code Content-Disposition}
+ * occur once per entity and so multiply by the part count.
+ * <p>
* Counts the fields actually reported to the caller: malformed fields the
* parser skips are not retained and so do not consume the budget.
*/
- private void checkTotalHeaderLimit() throws MimeException {
+ private void checkTotalHeaderLimits() throws MimeException {
int maxTotalHeaderCount = config.getMaxTotalHeaderCount();
totalHeaderCount++;
if (maxTotalHeaderCount > 0 && totalHeaderCount > maxTotalHeaderCount) {
throw new MaxHeaderLimitException("Maximum total header limit ("
+ maxTotalHeaderCount + ") exceeded");
}
+ long maxTotalHeaderLen = config.getMaxTotalHeaderLen();
+ if (maxTotalHeaderLen > 0) {
+ totalHeaderLen += rawLengthOf(currentStateMachine.getField());
+ if (totalHeaderLen > maxTotalHeaderLen) {
+ throw new MaxHeaderLengthLimitException("Maximum total header length limit ("
+ + maxTotalHeaderLen + ") exceeded");
+ }
+ }
+ }
+
+ /**
+ * Size of a field as it appeared on the wire. Falls back to the rendered length
+ * when a field carries no raw bytes, rather than calling {@code getSafeRaw()}
+ * which would allocate them just to measure them.
+ */
+ private static int rawLengthOf(Field field) {
+ ByteSequence raw = field.getRaw();
+ if (raw != null) {
+ return raw.length();
+ }
+ String name = field.getName();
+ String body = field.getBody();
+ return (name != null ? name.length() : 0) + (body != null ? body.length() : 0) + 4;
}
/**
diff --git a/core/src/test/java/org/apache/james/mime4j/stream/MimeEntityLimitsTest.java b/core/src/test/java/org/apache/james/mime4j/stream/MimeEntityLimitsTest.java
index bb6078be..f90e6d32 100644
--- a/core/src/test/java/org/apache/james/mime4j/stream/MimeEntityLimitsTest.java
+++ b/core/src/test/java/org/apache/james/mime4j/stream/MimeEntityLimitsTest.java
@@ -23,6 +23,7 @@ import java.io.ByteArrayInputStream;
import java.io.InputStream;
import org.apache.james.mime4j.Charsets;
+import org.apache.james.mime4j.io.MaxHeaderLengthLimitException;
import org.apache.james.mime4j.io.MaxHeaderLimitException;
import org.apache.james.mime4j.io.MaxNestingDepthLimitException;
import org.apache.james.mime4j.io.MaxPartCountLimitException;
@@ -344,15 +345,109 @@ public class MimeEntityLimitsTest {
}
}
+ /** A multipart whose every part carries one header of {@code bytes} octets. */
+ private static InputStream multipartWithFatHeaders(int parts, int bytes) {
+ StringBuilder pad = new StringBuilder("x: ");
+ while (pad.length() < bytes) {
+ pad.append('a');
+ }
+ StringBuilder sb = new StringBuilder();
+ sb.append("Content-Type: multipart/mixed; boundary=b\r\n\r\n");
+ for (int p = 0; p < parts; p++) {
+ sb.append("--b\r\n").append(pad).append("\r\n\r\n");
+ }
+ sb.append("--b--\r\n");
+ return new ByteArrayInputStream(sb.toString().getBytes(Charsets.US_ASCII));
+ }
+
+ @Test
+ public void totalHeaderLengthShouldAccumulateAcrossEntities() throws Exception {
+ // 20 parts x 1000 octets: no single header is anywhere near maxHeaderLen
+ MimeConfig config = MimeConfig.custom()
+ .setMaxLineLen(-1)
+ .setMaxHeaderLen(-1)
+ .setMaxTotalHeaderLen(10000)
+ .build();
+ try {
+ parse(config, multipartWithFatHeaders(20, 1000));
+ Assert.fail("MaxHeaderLengthLimitException expected");
+ } catch (MaxHeaderLengthLimitException expected) {
+ Assert.assertTrue(expected.getMessage(),
+ expected.getMessage().startsWith("Maximum total header length limit (10000)"));
+ }
+ }
+
+ @Test
+ public void totalHeaderLengthShouldAcceptAMessageWithinTheLimit() throws Exception {
+ MimeConfig config = MimeConfig.custom()
+ .setMaxLineLen(-1)
+ .setMaxHeaderLen(-1)
+ .setMaxTotalHeaderLen(1024 * 1024)
+ .build();
+ Assert.assertEquals(20, parse(config, multipartWithFatHeaders(20, 1000)));
+ }
+
+ @Test
+ public void totalHeaderLengthShouldBeDisabledWhenNegative() throws Exception {
+ MimeConfig config = MimeConfig.custom()
+ .setMaxLineLen(-1)
+ .setMaxHeaderLen(-1)
+ .setMaxTotalHeaderLen(-1)
+ .build();
+ Assert.assertEquals(20, parse(config, multipartWithFatHeaders(20, 100000)));
+ }
+
+ @Test
+ public void totalHeaderLengthShouldBeDisabledWhenZero() throws Exception {
+ MimeConfig config = MimeConfig.custom()
+ .setMaxLineLen(-1)
+ .setMaxHeaderLen(-1)
+ .setMaxTotalHeaderLen(0)
+ .build();
+ Assert.assertEquals(20, parse(config, multipartWithFatHeaders(20, 100000)));
+ }
+
+ @Test
+ public void defaultConfigShouldBoundTheTotalHeaderLength() throws Exception {
+ Assert.assertEquals(1024 * 1024, MimeConfig.DEFAULT.getMaxTotalHeaderLen());
+ Assert.assertEquals(1024 * 1024, MimeConfig.PERMISSIVE.getMaxTotalHeaderLen());
+ try {
+ // 512 parts x 8 KB of headers = 4 MB, no single header over 64 KB
+ parse(MimeConfig.PERMISSIVE, multipartWithFatHeaders(512, 8192));
+ Assert.fail("MaxHeaderLengthLimitException expected");
+ } catch (MaxHeaderLengthLimitException expected) {
+ // expected
+ }
+ }
+
+ @Test
+ public void permissiveConfigShouldBoundASingleHeaderLength() throws Exception {
+ Assert.assertEquals(64 * 1024, MimeConfig.PERMISSIVE.getMaxHeaderLen());
+ StringBuilder sb = new StringBuilder("x: ");
+ while (sb.length() < 64 * 1024 + 16) {
+ sb.append('a');
+ }
+ sb.append("\r\n\r\nbody\r\n");
+ InputStream in = new ByteArrayInputStream(sb.toString().getBytes(Charsets.US_ASCII));
+ try {
+ parse(MimeConfig.PERMISSIVE, in);
+ Assert.fail("MaxHeaderLengthLimitException expected");
+ } catch (MaxHeaderLengthLimitException expected) {
+ // expected
+ }
+ }
+
@Test
public void copyShouldCarryTheLimitsOver() {
MimeConfig config = MimeConfig.copy(MimeConfig.custom()
.setMaxPartCount(7)
.setMaxNestingDepth(9)
.setMaxTotalHeaderCount(11)
+ .setMaxTotalHeaderLen(13)
.build()).build();
Assert.assertEquals(7, config.getMaxPartCount());
Assert.assertEquals(9, config.getMaxNestingDepth());
Assert.assertEquals(11, config.getMaxTotalHeaderCount());
+ Assert.assertEquals(13, config.getMaxTotalHeaderLen());
}
}
diff --git a/dom/src/test/java/org/apache/james/mime4j/dom/LargeMessageParsingTest.java b/dom/src/test/java/org/apache/james/mime4j/dom/LargeMessageParsingTest.java
index b3067e16..4a27e326 100644
--- a/dom/src/test/java/org/apache/james/mime4j/dom/LargeMessageParsingTest.java
+++ b/dom/src/test/java/org/apache/james/mime4j/dom/LargeMessageParsingTest.java
@@ -23,6 +23,7 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import org.apache.james.mime4j.MimeIOException;
+import org.apache.james.mime4j.io.MaxHeaderLengthLimitException;
import org.apache.james.mime4j.io.MaxHeaderLimitException;
import org.apache.james.mime4j.message.DefaultMessageBuilder;
import org.apache.james.mime4j.stream.MimeConfig;
@@ -74,25 +75,26 @@ public class LargeMessageParsingTest {
@Test
public void parsingAMessageWithLongLinesWithPermissiveConfigShouldSucceed() throws Exception {
- ByteArrayOutputStream longLineOutputStream = new ByteArrayOutputStream( 1024 * 1024);
- ByteArrayOutputStream longHeaderOutputStream = new ByteArrayOutputStream( 1024 * 1024);
+ ByteArrayOutputStream longLineOutputStream = new ByteArrayOutputStream(1024 * 1024);
+ ByteArrayOutputStream longHeaderOutputStream = new ByteArrayOutputStream(1024 * 1024);
longHeaderOutputStream.write("header: ".getBytes());
- // Each header is ~ 500 Ko
- for (int i = 0; i < 50 * 1024; i++) {
+ // Each header stays just under the permissive per header cap
+ while (longHeaderOutputStream.size() < MimeConfig.PERMISSIVE.getMaxHeaderLen() - 32) {
longHeaderOutputStream.write("0123456789".getBytes());
}
longHeaderOutputStream.write("\r\n".getBytes());
- // Each line is ~ 1Mo
+ // Each line is ~ 1Mo: long lines are still unbounded under the permissive profile
for (int i = 0; i < 100 * 1024; i++) {
longLineOutputStream.write("0123456789".getBytes());
}
longLineOutputStream.write("\r\n".getBytes());
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(100 * 1024 * 1024);
- // 60 * 0.5 = ~ 30 Mo of headers
- for (int i = 0; i < 60; i++) {
+ // as many ~64 Ko headers as the total header budget allows
+ long headers = MimeConfig.PERMISSIVE.getMaxTotalHeaderLen() / longHeaderOutputStream.size();
+ for (int i = 0; i < headers; i++) {
outputStream.write(longHeaderOutputStream.toByteArray());
}
outputStream.write("\r\n".getBytes());
@@ -105,4 +107,27 @@ public class LargeMessageParsingTest {
messageBuilder.setMimeEntityConfig(MimeConfig.PERMISSIVE);
messageBuilder.parseMessage(new ByteArrayInputStream(outputStream.toByteArray()));
}
+
+ @Test
+ public void parsingAnOversizedHeaderWithPermissiveConfigShouldBeRejected() throws Exception {
+ // An address, group or parameter list is retained as one object per item, so
+ // a single unbounded header amplifies without limit. 500 Ko in one field used
+ // to be accepted; MIME4J-269's "denying a single email to use all JVM memory"
+ // covers this too.
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream(1024 * 1024);
+ outputStream.write("header: ".getBytes());
+ for (int i = 0; i < 50 * 1024; i++) {
+ outputStream.write("0123456789".getBytes());
+ }
+ outputStream.write("\r\n\r\nbody\r\n".getBytes());
+
+ DefaultMessageBuilder messageBuilder = new DefaultMessageBuilder();
+ messageBuilder.setMimeEntityConfig(MimeConfig.PERMISSIVE);
+ try {
+ messageBuilder.parseMessage(new ByteArrayInputStream(outputStream.toByteArray()));
+ Assert.fail("MimeIOException expected");
+ } catch (MimeIOException e) {
+ Assert.assertTrue(e.getCause() instanceof MaxHeaderLengthLimitException);
+ }
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org