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 23a562ac629abf7527034e8e9e051675873e5baf Author: Benoit TELLIER <[email protected]> AuthorDate: Wed Nov 4 14:52:39 2026 +0100 [PERF] LenientAddressParser: do not rescan for ']' after each unclosed '[' parseDomain called copyDomainLiteral at every '['. When no ']' follows, the scan ran to the next delimiter or the end of the value and was repeated for the next '[': O(k*n) for k unclosed brackets, over 16 s for a 32 KB To header under the fuzzer. copyDomainLiteral now returns where a failed scan stopped. Every '[' before that index is unclosed as well, so parseDomain copies it as content without scanning again. Found by twake-fuzz, scenario 33-header-value-cost-scaling. --- .../mime4j/field/address/LenientAddressParser.java | 17 +++++++++++------ .../field/address/LenientAddressBuilderTest.java | 18 ++++++++++++++++++ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/dom/src/main/java/org/apache/james/mime4j/field/address/LenientAddressParser.java b/dom/src/main/java/org/apache/james/mime4j/field/address/LenientAddressParser.java index e184eab6..761b995b 100644 --- a/dom/src/main/java/org/apache/james/mime4j/field/address/LenientAddressParser.java +++ b/dom/src/main/java/org/apache/james/mime4j/field/address/LenientAddressParser.java @@ -90,6 +90,9 @@ public class LenientAddressParser implements AddressParser { String parseDomain(final ByteSequence buf, final ParserCursor cursor, final BitSet delimiters) { StringBuilder dst = new StringBuilder(); + // Every '[' before this index has no ']' before the next delimiter: a failed scan for + // the closing bracket is not repeated for each of them, which would be quadratic. + int unclosedUntil = -1; while (!cursor.atEnd()) { char current = (char) (buf.byteAt(cursor.getPos()) & 0xff); if (delimiters != null && delimiters.get(current)) { @@ -98,7 +101,8 @@ public class LenientAddressParser implements AddressParser { this.parser.skipWhiteSpace(buf, cursor); } else if (current == '(') { this.parser.skipComment(buf, cursor); - } else if (current == '[' && copyDomainLiteral(buf, cursor, delimiters, dst)) { + } else if (current == '[' && cursor.getPos() >= unclosedUntil + && (unclosedUntil = copyDomainLiteral(buf, cursor, delimiters, dst)) < 0) { continue; } else { this.parser.copyContent(buf, cursor, delimiters, dst); @@ -109,9 +113,10 @@ public class LenientAddressParser implements AddressParser { /** * Copies a domain literal up to its closing bracket: '(' and ')' are dtext there, not a comment - * (RFC 5322 section 3.4.1). Returns false, copying nothing, when no ']' comes before a delimiter. + * (RFC 5322 section 3.4.1). Returns -1 once copied. When no ']' comes before a delimiter, copies + * nothing and returns the index where the search stopped: the delimiter, or the upper bound. */ - private boolean copyDomainLiteral(final ByteSequence buf, final ParserCursor cursor, final BitSet delimiters, + private int copyDomainLiteral(final ByteSequence buf, final ParserCursor cursor, final BitSet delimiters, final StringBuilder dst) { int from = cursor.getPos(); int to = cursor.getUpperBound(); @@ -128,13 +133,13 @@ public class LenientAddressParser implements AddressParser { // UTF-8, as the other parts of the header dst.append(ContentUtil.decode(StandardCharsets.UTF_8, literal)); cursor.updatePos(i + 1); - return true; + return -1; } if (delimiters != null && delimiters.get(current)) { - return false; + return i; } } - return false; + return to; } DomainList parseRoute(final ByteSequence buf, final ParserCursor cursor, final BitSet delimiters) { diff --git a/dom/src/test/java/org/apache/james/mime4j/field/address/LenientAddressBuilderTest.java b/dom/src/test/java/org/apache/james/mime4j/field/address/LenientAddressBuilderTest.java index d3e21c69..651267ac 100644 --- a/dom/src/test/java/org/apache/james/mime4j/field/address/LenientAddressBuilderTest.java +++ b/dom/src/test/java/org/apache/james/mime4j/field/address/LenientAddressBuilderTest.java @@ -647,4 +647,22 @@ public class LenientAddressBuilderTest { Assert.assertEquals("a", AddressFormatter.DEFAULT.encode(new Mailbox("a", ",;"))); Assert.assertEquals("John <a>", AddressFormatter.DEFAULT.encode(new Mailbox("John", "a", "<>"))); } + + @Test(timeout = 2000) + public void parseAddressListShouldBeLinearOnManyUnclosedDomainLiterals() { + StringBuilder sb = new StringBuilder("a@b"); + for (int i = 0; i < 100000; i++) { + sb.append("[x "); + } + AddressList list = parser.parseAddressList(sb); + Assert.assertEquals(1, list.size()); + Assert.assertEquals("b[x[x[x", ((Mailbox) list.get(0)).getDomain().substring(0, 7)); + } + + @Test + public void anUnclosedBracketShouldNotHideALaterDomainLiteral() { + AddressList list = parser.parseAddressList("a@b[x [y, c@[1.2.3.4]"); + Assert.assertEquals(2, list.size()); + Assert.assertEquals("[1.2.3.4]", ((Mailbox) list.get(1)).getDomain()); + } } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
