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

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


The following commit(s) were added to refs/heads/master by this push:
     new 2ab06c3f6d JAMES-4034 SMTP submission: validate FROM header (#2246)
2ab06c3f6d is described below

commit 2ab06c3f6daa427e4f5e2f7c5897ce51f12c6a55
Author: Benoit TELLIER <[email protected]>
AuthorDate: Fri May 17 09:12:56 2024 +0200

    JAMES-4034 SMTP submission: validate FROM header (#2246)
---
 ...AbstractSenderAuthIdentifyVerificationHook.java |  2 +-
 .../james/smtp/SmtpIdentityVerificationTest.java   | 68 ++++++++++++++++++++++
 .../SenderAuthIdentifyVerificationHook.java        | 60 ++++++++++++++++++-
 3 files changed, 128 insertions(+), 2 deletions(-)

diff --git 
a/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/core/AbstractSenderAuthIdentifyVerificationHook.java
 
b/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/core/AbstractSenderAuthIdentifyVerificationHook.java
index edf54e4c5f..a79899fac9 100644
--- 
a/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/core/AbstractSenderAuthIdentifyVerificationHook.java
+++ 
b/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/core/AbstractSenderAuthIdentifyVerificationHook.java
@@ -36,7 +36,7 @@ import com.google.common.base.Preconditions;
  * Handler which check if the authenticated user is the same as the one used 
as MAIL FROM
  */
 public abstract class AbstractSenderAuthIdentifyVerificationHook implements 
MailHook, RcptHook {
-    private static final HookResult INVALID_AUTH = HookResult.builder()
+    protected static final HookResult INVALID_AUTH = HookResult.builder()
         .hookReturnCode(HookReturnCode.deny())
         .smtpReturnCode(SMTPRetCode.BAD_SEQUENCE)
         .smtpDescription(DSNStatus.getStatus(DSNStatus.PERMANENT, 
DSNStatus.SECURITY_AUTH)
diff --git 
a/server/mailet/integration-testing/src/test/java/org/apache/james/smtp/SmtpIdentityVerificationTest.java
 
b/server/mailet/integration-testing/src/test/java/org/apache/james/smtp/SmtpIdentityVerificationTest.java
index 54d14c72e3..1fa9874a6c 100644
--- 
a/server/mailet/integration-testing/src/test/java/org/apache/james/smtp/SmtpIdentityVerificationTest.java
+++ 
b/server/mailet/integration-testing/src/test/java/org/apache/james/smtp/SmtpIdentityVerificationTest.java
@@ -40,6 +40,8 @@ import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.RegisterExtension;
 import org.junit.jupiter.api.io.TempDir;
 
+import com.google.common.collect.ImmutableList;
+
 class SmtpIdentityVerificationTest {
     private static final String ATTACKER_PASSWORD = "secret";
 
@@ -110,6 +112,72 @@ class SmtpIdentityVerificationTest {
             .authenticate(USER, PASSWORD).sendMessage(USER, USER);
     }
 
+    @Test
+    void spoofingAttemptsShouldBeRejectedInFromField(@TempDir File 
temporaryFolder) throws Exception {
+        createJamesServer(temporaryFolder, SmtpConfiguration.builder()
+            .requireAuthentication()
+            .verifyIdentity());
+
+        String message = """
+            FROM: [email protected]\r
+            subject: test\r
+            \r
+            content\r
+            .\r
+            """;
+
+        assertThatThrownBy(() ->
+            messageSender.connect(LOCALHOST_IP, 
jamesServer.getProbe(SmtpGuiceProbe.class).getSmtpPort())
+                .authenticate(USER, PASSWORD)
+                .sendMessageWithHeaders(USER, ImmutableList.of(USER), message))
+            .isInstanceOf(SMTPSendingException.class)
+            .hasMessageContaining("503 5.7.1 Incorrect Authentication for 
Specified Email Address");
+    }
+
+    @Test
+    void spoofingInternalAddressAttemptsShouldBeRejectedInFromField(@TempDir 
File temporaryFolder) throws Exception {
+        createJamesServer(temporaryFolder, SmtpConfiguration.builder()
+            .requireAuthentication()
+            .verifyIdentity());
+
+        String message = """
+            FROM: [email protected]\r
+            subject: test\r
+            \r
+            content\r
+            .\r
+            """;
+
+        assertThatThrownBy(() ->
+            messageSender.connect(LOCALHOST_IP, 
jamesServer.getProbe(SmtpGuiceProbe.class).getSmtpPort())
+                .authenticate(USER, PASSWORD)
+                .sendMessageWithHeaders(USER, ImmutableList.of(USER), message))
+            .isInstanceOf(SMTPSendingException.class)
+            .hasMessageContaining("503 5.7.1 Incorrect Authentication for 
Specified Email Address");
+    }
+
+    @Test
+    void aliasShouldBeSupportedInFromField(@TempDir File temporaryFolder) 
throws Exception {
+        createJamesServer(temporaryFolder, SmtpConfiguration.builder()
+            .requireAuthentication()
+            .verifyIdentity());
+
+        jamesServer.getProbe(DataProbeImpl.class)
+            .addUserAliasMapping("alias", DEFAULT_DOMAIN, USER);
+
+        String message = """
+            FROM: [email protected]\r
+            subject: test\r
+            \r
+            content\r
+            .\r
+            """;
+
+        messageSender.connect(LOCALHOST_IP, 
jamesServer.getProbe(SmtpGuiceProbe.class).getSmtpPort())
+            .authenticate(USER, PASSWORD)
+            .sendMessageWithHeaders(USER, ImmutableList.of(USER), message);
+    }
+
     @Test
     void verifyIdentityShouldRejectNullSenderWHenAuthenticated(@TempDir File 
temporaryFolder) throws Exception {
         createJamesServer(temporaryFolder, SmtpConfiguration.builder()
diff --git 
a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/SenderAuthIdentifyVerificationHook.java
 
b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/SenderAuthIdentifyVerificationHook.java
index 690b2266b4..e4d53109a7 100644
--- 
a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/SenderAuthIdentifyVerificationHook.java
+++ 
b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/SenderAuthIdentifyVerificationHook.java
@@ -18,7 +18,13 @@
  ****************************************************************/
 package org.apache.james.smtpserver;
 
+import java.util.stream.Stream;
+
 import jakarta.inject.Inject;
+import jakarta.mail.Address;
+import jakarta.mail.MessagingException;
+import jakarta.mail.internet.AddressException;
+import jakarta.mail.internet.InternetAddress;
 
 import org.apache.james.core.Domain;
 import org.apache.james.core.MailAddress;
@@ -32,13 +38,15 @@ import org.apache.james.protocols.smtp.hook.HookResult;
 import org.apache.james.rrt.api.CanSendFrom;
 import org.apache.james.user.api.UsersRepository;
 import org.apache.james.user.api.UsersRepositoryException;
+import org.apache.james.util.StreamUtils;
+import org.apache.mailet.Mail;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
  * Handler which check if the authenticated user is incorrect
  */
-public class SenderAuthIdentifyVerificationHook extends 
AbstractSenderAuthIdentifyVerificationHook {
+public class SenderAuthIdentifyVerificationHook extends 
AbstractSenderAuthIdentifyVerificationHook implements JamesMessageHook {
     private static final Logger LOGGER = 
LoggerFactory.getLogger(SenderAuthIdentifyVerificationHook.class);
 
     private final DomainList domains;
@@ -90,4 +98,54 @@ public class SenderAuthIdentifyVerificationHook extends 
AbstractSenderAuthIdenti
         }
         return allowed;
     }
+
+    @Override
+    public HookResult onMessage(SMTPSession session, Mail mail) {
+        ExtendedSMTPSession nSession = (ExtendedSMTPSession) session;
+        if (nSession.verifyIdentity()) {
+            try {
+                return StreamUtils.ofNullable(mail.getMessage().getFrom())
+                    .distinct()
+                    .flatMap(address -> doCheckMessage(session, address))
+                    .findFirst()
+                    .orElse(HookResult.DECLINED);
+            } catch (MessagingException e) {
+                throw new RuntimeException(e);
+            }
+        } else {
+            return HookResult.DECLINED;
+        }
+    }
+
+    private Stream<HookResult> doCheckMessage(SMTPSession session, Address 
from) {
+        if (fromDoesNotMatchAuthUser(session, from)) {
+            return Stream.of(INVALID_AUTH);
+        } else {
+            return Stream.empty();
+        }
+    }
+
+    private boolean fromDoesNotMatchAuthUser(SMTPSession session, Address 
from) {
+        if (from instanceof InternetAddress internetAddress) {
+            try {
+                MailAddress mailAddress = new 
MailAddress(internetAddress.getAddress());
+                return session.getUsername() != null &&
+                    (!fromMatchSessionUser(mailAddress, session) || 
!belongsToLocalDomain(mailAddress));
+            } catch (AddressException e) {
+                // Never happens as valid InternetAddress are valid MailAddress
+                throw new RuntimeException(e);
+            }
+        }
+        return false;
+    }
+
+    private boolean fromMatchSessionUser(MailAddress from, SMTPSession 
session) {
+        Username authUser = session.getUsername();
+        Username sender = getUser(from);
+        return isSenderAllowed(authUser, sender);
+    }
+    
+    private boolean belongsToLocalDomain(MailAddress from) {
+        return isLocalDomain(from.getDomain());
+    }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to