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-jdkim.git
commit 51dea22d51edb431bab6108dfd4f789023b242ad Author: Benoit TELLIER <[email protected]> AuthorDate: Wed Nov 4 18:19:19 2026 +0100 [FIX] SignatureRecordImpl: require well-formed UTF-8 in i= and z= RFC 8616 section 4 adds UTF8-non-ascii to the i= local part and to the z= copies, that is well-formed UTF-8. The checks are per character class: any octet above 0x7F passed, so a lone continuation octet, a truncated or overlong sequence, an encoded surrogate or F5-FF made a signature that verifies, raw or as =XX. checkIdentity and checkCopiedHeaderFields now decode the octets of the decoded value with a reporting UTF-8 decoder, so both forms are checked alike. --- .../james/jdkim/tagvalue/SignatureRecordImpl.java | 39 ++++++++++++++++++++++ .../james/jdkim/SignatureRecordImplTest.java | 33 ++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/main/src/main/java/org/apache/james/jdkim/tagvalue/SignatureRecordImpl.java b/main/src/main/java/org/apache/james/jdkim/tagvalue/SignatureRecordImpl.java index 1154357..d2ee419 100644 --- a/main/src/main/java/org/apache/james/jdkim/tagvalue/SignatureRecordImpl.java +++ b/main/src/main/java/org/apache/james/jdkim/tagvalue/SignatureRecordImpl.java @@ -24,6 +24,10 @@ import static org.apache.james.jdkim.parser.DKIMQuotedPrintable.dkimQuotedPrinta import org.apache.james.jdkim.api.HashMethod; import org.apache.james.jdkim.api.SignatureRecord; +import java.nio.ByteBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.Collections; import java.util.LinkedHashMap; @@ -135,6 +139,10 @@ public class SignatureRecordImpl extends TagValue implements SignatureRecord { throw new IllegalStateException("Invalid i= [" + value + "]: the local part is neither a Dot-string" + " nor a Quoted-string (RFC 5321)"); } + if (!isWellFormedUtf8(localPart)) { + throw new IllegalStateException("Invalid i= [" + value + "]: the local part is not well-formed UTF-8" + + " (RFC 8616 section 4)"); + } checkDomainName("i= domain", value.substring(at + 1), 2); } @@ -165,6 +173,37 @@ public class SignatureRecordImpl extends TagValue implements SignatureRecord { throw new IllegalStateException("Invalid z= [" + z + "]: copy [" + copy + "] is not hdr-name [FWS] \":\" qp-hdr-value (RFC 6376 section 3.5)"); } + if (!isWellFormedUtf8(dkimQuotedPrintableDecode(trimmed))) { + throw new IllegalStateException("Invalid z= [" + z + "]: copy [" + copy + + "] is not well-formed UTF-8 (RFC 8616 section 4)"); + } + } + } + + /** + * RFC 8616 section 4 admits UTF8-non-ascii (RFC 6532 section 3.1): the octets above 0x7F must form + * well-formed UTF-8. The header octets reach the tag values as ISO-8859-1 chars. + * + * @param octets a decoded tag value, one char per octet + * @return whether it is well-formed UTF-8 + */ + static boolean isWellFormedUtf8(CharSequence octets) { + byte[] bytes = new byte[octets.length()]; + for (int k = 0; k < bytes.length; k++) { + char c = octets.charAt(k); + if (c > 0xFF) { + return false; + } + bytes[k] = (byte) c; + } + try { + StandardCharsets.UTF_8.newDecoder() + .onMalformedInput(CodingErrorAction.REPORT) + .onUnmappableCharacter(CodingErrorAction.REPORT) + .decode(ByteBuffer.wrap(bytes)); + return true; + } catch (CharacterCodingException e) { + return false; } } diff --git a/main/src/test/java/org/apache/james/jdkim/SignatureRecordImplTest.java b/main/src/test/java/org/apache/james/jdkim/SignatureRecordImplTest.java index a36ff08..d213ee0 100644 --- a/main/src/test/java/org/apache/james/jdkim/SignatureRecordImplTest.java +++ b/main/src/test/java/org/apache/james/jdkim/SignatureRecordImplTest.java @@ -475,4 +475,37 @@ public class SignatureRecordImplTest { recordWithIdentity(repeat("a.", 32768) + "[email protected]"); recordWithIdentity("\"" + repeat("a=20", 16384) + "\"@example.com"); } + + @Test + public void illFormedUtf8IdentityIsASyntaxError() { + // header octets arrive as ISO-8859-1 chars, as Message produces them + for (String i : new String[] { + "\"\u0080\"@example.com", // lone continuation octet, raw + "\"=80\"@example.com", // the same, DKIM-Quoted-Printable + "\[email protected]", // truncated 2-octet sequence + "\u00c0\[email protected]", // overlong "/" + "\u00ed\u00a0\[email protected]", // encoded surrogate U+D800 + "\[email protected]"}) { // never valid in UTF-8 + assertThatThrownBy(() -> recordWithIdentity(i)).as("i=%s", i) + .isInstanceOf(IllegalStateException.class); + } + } + + @Test + public void illFormedUtf8CopiedFieldIsASyntaxError() { + for (String z : new String[] {"Subject:\u0080", "Subject:=80", "From:[email protected]|Subject:\u00c3"}) { + assertThatThrownBy(() -> recordWithCopiedFields(z)).as("z=%s", z) + .isInstanceOf(IllegalStateException.class); + } + } + + @Test + public void wellFormedUtf8IdentityIsStillAccepted() { + // "\u03b4" = CE B4 + assertThat(recordWithIdentity("\u00ce\[email protected]").getIdentity().toString()) + .isEqualTo("\u00ce\[email protected]"); + assertThat(recordWithIdentity("[email protected]").getIdentity().toString()) + .isEqualTo("\u00ce\[email protected]"); + recordWithCopiedFields("Subject:=CE=B4|To:\u00ce\[email protected]"); + } } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
