You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by rc...@apache.org on 2023/12/25 03:14:01 UTC

(james-project) branch 3.7.x updated: [FIX] Enforce CRLF as part of SMTP DATA transaction (#1878)

This is an automated email from the ASF dual-hosted git repository.

rcordier pushed a commit to branch 3.7.x
in repository https://gitbox.apache.org/repos/asf/james-project.git


The following commit(s) were added to refs/heads/3.7.x by this push:
     new d1ef102540 [FIX] Enforce CRLF as part of SMTP DATA transaction (#1878)
d1ef102540 is described below

commit d1ef102540e504c067b6c1721a6f1e7eee9c6fc6
Author: Benoit TELLIER <bt...@linagora.com>
AuthorDate: Mon Dec 25 04:13:56 2023 +0100

    [FIX] Enforce CRLF as part of SMTP DATA transaction (#1878)
---
 .../java/org/apache/james/LmtpIntegrationTest.java | 26 ++++++++++++++++++++++
 .../DataLineJamesMessageHookHandler.java           | 24 ++++++++++++++++++++
 2 files changed, 50 insertions(+)

diff --git a/server/apps/memory-app/src/test/java/org/apache/james/LmtpIntegrationTest.java b/server/apps/memory-app/src/test/java/org/apache/james/LmtpIntegrationTest.java
index 134c7fa186..a11074481a 100644
--- a/server/apps/memory-app/src/test/java/org/apache/james/LmtpIntegrationTest.java
+++ b/server/apps/memory-app/src/test/java/org/apache/james/LmtpIntegrationTest.java
@@ -85,6 +85,32 @@ public class LmtpIntegrationTest {
                 "451 4.0.0 Temporary error deliver message <er...@domain.tld>");
     }
 
+    @Test
+    void preventSMTPSmugglingAttacksByEnforcingCRLF(GuiceJamesServer guiceJamesServer) throws Exception {
+        SocketChannel server = SocketChannel.open();
+        server.connect(new InetSocketAddress(LOCALHOST_IP, guiceJamesServer.getProbe(LmtpGuiceProbe.class).getLmtpPort()));
+        readBytes(server);
+
+        server.write(ByteBuffer.wrap(("LHLO <" + DOMAIN + ">\r\n").getBytes(StandardCharsets.UTF_8)));
+        readBytes(server);
+        server.write(ByteBuffer.wrap(("MAIL FROM: <user@" + DOMAIN + ">\r\n").getBytes(StandardCharsets.UTF_8)));
+        readBytes(server);
+        server.write(ByteBuffer.wrap(("RCPT TO: <user@" + DOMAIN + ">\r\n").getBytes(StandardCharsets.UTF_8)));
+        readBytes(server);
+        server.write(ByteBuffer.wrap(("DATA\r\n").getBytes(StandardCharsets.UTF_8)));
+        readBytes(server); // needed to synchronize
+        server.write(ByteBuffer.wrap((
+            "header:value\r\n\r\nbody 1\r\n.\nMAIL FROM: <a...@toto.com>\r\n" +
+                "RCPT TO: <us...@domain.tld>\r\n" +
+                "DATA\r\n" +
+                "header: yolo 2\r\n" +
+                "\r\nbody 2\r\n.\r\n").getBytes(StandardCharsets.UTF_8)));
+        byte[] dataResponse = readBytes(server);
+        server.write(ByteBuffer.wrap(("QUIT\r\n").getBytes(StandardCharsets.UTF_8)));
+
+        assertThat(new String(dataResponse, StandardCharsets.UTF_8))
+            .contains("500 5.0.0 line delimiter must be CRLF");
+    }
 
     private byte[] readBytes(SocketChannel channel) throws IOException {
         ByteBuffer line = ByteBuffer.allocate(1024);
diff --git a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/DataLineJamesMessageHookHandler.java b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/DataLineJamesMessageHookHandler.java
index f71f11ece3..44d15b2fbc 100644
--- a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/DataLineJamesMessageHookHandler.java
+++ b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/DataLineJamesMessageHookHandler.java
@@ -37,6 +37,7 @@ import org.apache.james.protocols.api.Response;
 import org.apache.james.protocols.api.handler.ExtensibleHandler;
 import org.apache.james.protocols.api.handler.LineHandler;
 import org.apache.james.protocols.api.handler.WiringException;
+import org.apache.james.protocols.netty.CommandInjectionDetectedException;
 import org.apache.james.protocols.smtp.MailEnvelope;
 import org.apache.james.protocols.smtp.SMTPResponse;
 import org.apache.james.protocols.smtp.SMTPRetCode;
@@ -62,6 +63,22 @@ import com.google.common.collect.ImmutableList;
  */
 public class DataLineJamesMessageHookHandler implements DataLineFilter, ExtensibleHandler {
     private static final Logger LOGGER = LoggerFactory.getLogger(DataLineJamesMessageHookHandler.class);
+    public static final boolean DETECT_SMTP_SMUGGLING = System.getProperty("james.prevent.smtp.smuggling", "true").equals("true");
+
+    /*
+    SMTP smuggling: https://sec-consult.com/blog/detail/smtp-smuggling-spoofing-e-mails-worldwide/
+    Strict CRLF enforcement rationals: https://haraka.github.io/barelf
+     */
+    private static void detectSMTPSmuggling(byte[] line) {
+        if (DETECT_SMTP_SMUGGLING) {
+            if (line.length < 2
+                || line[line.length - 2] != '\r'
+                || line[line.length - 1] != '\n') {
+
+                throw new CommandInjectionDetectedException();
+            }
+        }
+    }
 
     private List<JamesMessageHook> messageHandlers;
 
@@ -79,6 +96,7 @@ public class DataLineJamesMessageHookHandler implements DataLineFilter, Extensib
             .orElseThrow(() -> new RuntimeException("'" + SMTPConstants.DATA_MIMEMESSAGE_STREAMSOURCE.asString() + "' has not been filled."));
 
         try {
+            detectSMTPSmuggling(line);
             OutputStream out = mmiss.getWritableOutputStream();
 
             // 46 is "."
@@ -134,6 +152,12 @@ public class DataLineJamesMessageHookHandler implements DataLineFilter, Extensib
             SMTPResponse response = new SMTPResponse(SMTPRetCode.LOCAL_ERROR, DSNStatus.getStatus(DSNStatus.TRANSIENT, DSNStatus.UNDEFINED_STATUS) + " Error processing message: " + e.getMessage());
             LOGGER.error("Unknown error occurred while processing DATA.", e);
             return response;
+        } catch (CommandInjectionDetectedException e) {
+            LifecycleUtil.dispose(mmiss);
+            SMTPResponse response = new SMTPResponse(SMTPRetCode.SYNTAX_ERROR_COMMAND_UNRECOGNIZED, DSNStatus.getStatus(DSNStatus.PERMANENT, DSNStatus.UNDEFINED_STATUS) + " line delimiter must be CRLF");
+            LOGGER.info("Use of CRLF, which might indicate SMTP smuggling attempt");
+            return response;
+
         }
         return null;
     }


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org