VYPR
High severityNVD Advisory· Published Feb 27, 2024· Updated Feb 13, 2025

SMTP smuggling in Apache James

CVE-2023-51747

Description

Apache James prior to versions 3.8.1 and 3.7.5 is vulnerable to SMTP smuggling.

A lenient behaviour in line delimiter handling might create a difference of interpretation between the sender and the receiver which can be exploited by an attacker to forge an SMTP envelop, allowing for instance to bypass SPF checks.

The patch implies enforcement of CRLF as a line delimiter as part of the DATA transaction.

We recommend James users to upgrade to non vulnerable versions.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Apache James prior to versions 3.8.1 and 3.7.5 is vulnerable to SMTP smuggling due to lenient line delimiter handling, allowing email spoofing and SPF bypass.

Vulnerability

Description

CVE-2023-51747 affects Apache James, a Java SMTP server, prior to versions 3.8.1 and 3.7.5. The vulnerability is a classic SMTP smuggling issue rooted in the server's lenient handling of line delimiters during the DATA transaction. Officially, SMTP uses CRLF (0x0D0A) as the line delimiter, but some implementations, including older versions of Apache James, accept bare LF (0x0A) as a valid line terminator [1][2]. This discrepancy between sender and receiver behavior is the root cause of the vulnerability.

Exploitation

An attacker can exploit this by crafting an email message that contains a non-standard End-of-DATA sequence, such as . or ., instead of the standard . [3]. This tricks the receiving server into interpreting the message as terminated early, allowing the attacker to inject additional SMTP commands (like MAIL FROM, RCPT TO, DATA) within what was supposed to be the email body. The attack does not require authentication against the vulnerable server itself; rather, it exploits the composition of a sending MTA (that allows bare LF) and a receiving MTA (that expects strict CRLF) [3][4].

Impact

Successful exploitation enables an attacker to send emails with a forged SMTP envelope, effectively spoofing the sender address. This bypasses SPF (Sender Policy Framework) checks because the attacking server sends the spoofed message from its own IP, which may be authorized to send mail on behalf of the forged domain [1][2]. The impact is that recipients see the email as coming from a legitimate source, making phishing and social engineering attacks more effective.

Mitigation

The fix for Apache James enforces strict CRLF (0x0D0A) as the only allowed line delimiter during the DATA transaction, as shown in the commit to the James project [4]. Users must upgrade to versions 3.8.1 or 3.7.5 (the two release lines that contain the patch). There is no mention of a workaround if upgrading is not immediately possible, so patching is the recommended action. The vulnerability was disclosed in December 2023 by SEC Consult as part of a broader SMTP smuggling research effort that also affected other MTAs like Postfix, Sendmail, and Exim [1][3].

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
org.apache.james:james-serverMaven
< 3.7.53.7.5
org.apache.james:james-serverMaven
>= 3.8.0, < 3.8.13.8.1

Affected products

2

Patches

2
d5cd8bb098aa

[FIX] Enforce CRLF as part of SMTP DATA transaction (#1877)

https://github.com/apache/james-projectBenoit TELLIERDec 25, 2023via ghsa
2 files changed · +51 0
  • server/apps/memory-app/src/test/java/org/apache/james/LmtpIntegrationTest.java+26 0 modified
    @@ -85,6 +85,32 @@ void lmtpShouldBeConfigurableToReport(GuiceJamesServer guiceJamesServer) throws
                     "451 4.0.0 Temporary error deliver message <error@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: <user@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);
    
  • server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/DataLineJamesMessageHookHandler.java+25 0 modified
    @@ -36,6 +36,7 @@
     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;
    @@ -61,6 +62,22 @@
      */
     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;
     
    @@ -70,11 +87,13 @@ public class DataLineJamesMessageHookHandler implements DataLineFilter, Extensib
     
         @Override
         public Response onLine(SMTPSession session, byte[] line, LineHandler<SMTPSession> next) {
    +
             ExtendedSMTPSession extendedSMTPSession = (ExtendedSMTPSession) session;
             MimeMessageInputStreamSource mmiss = extendedSMTPSession.getMimeMessageWriter();
     
             try {
                 OutputStream out = mmiss.getWritableOutputStream();
    +            detectSMTPSmuggling(line);
     
                 // 46 is "."
                 // Stream terminated
    @@ -129,6 +148,12 @@ public Response onLine(SMTPSession session, byte[] line, LineHandler<SMTPSession
                 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;
         }
    
d1ef102540e5

[FIX] Enforce CRLF as part of SMTP DATA transaction (#1878)

https://github.com/apache/james-projectBenoit TELLIERDec 25, 2023via ghsa
2 files changed · +50 0
  • server/apps/memory-app/src/test/java/org/apache/james/LmtpIntegrationTest.java+26 0 modified
    @@ -85,6 +85,32 @@ void lmtpShouldBeConfigurableToReport(GuiceJamesServer guiceJamesServer) throws
                     "451 4.0.0 Temporary error deliver message <error@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: <user@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);
    
  • server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/DataLineJamesMessageHookHandler.java+24 0 modified
    @@ -37,6 +37,7 @@
     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 @@
      */
     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 Response onLine(SMTPSession session, ByteBuffer lineByteBuffer, LineHandl
                 .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 Response onLine(SMTPSession session, ByteBuffer lineByteBuffer, LineHandl
                 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;
         }
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

9

News mentions

0

No linked articles in our index yet.