From 0d0f97ea59e841efdd7ce13f613442dabd5d7d27 Mon Sep 17 00:00:00 2001 From: Mario Fetka Date: Tue, 28 Jul 2026 07:45:31 +0200 Subject: [PATCH] smtp: distinguish bad paths from unknown users --- src/agents/smtp/protocol.c | 44 ++++++++++++++++++++++++++- src/agents/smtp/smtpd.c | 3 +- src/agents/smtp/tests/protocol-test.c | 10 ++++++ 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/agents/smtp/protocol.c b/src/agents/smtp/protocol.c index 5e535d4..168c464 100644 --- a/src/agents/smtp/protocol.c +++ b/src/agents/smtp/protocol.c @@ -233,7 +233,49 @@ SMTPPathIsSafe(const char *path) int SMTPRecipientPathIsValid(const char *path) { - return path != NULL && path[0] != '\0' && SMTPPathIsSafe(path); + const unsigned char *cursor = (const unsigned char *)path; + const unsigned char *separator = NULL; + const unsigned char *local_end; + int previous_dot = 0; + + if (cursor == NULL || *cursor == '\0' || !SMTPPathIsSafe(path)) return 0; + + /* + * RewriteAddress() understands obsolete percent routes, but accepting a + * second raw '@' here would turn malformed SMTP input into a routing + * lookup and make it indistinguishable from an unknown mailbox. Its + * resolver protocol cannot preserve quoted local-parts, so accept the + * RFC 5321 dot-string form here rather than silently changing one. + */ + local_end = cursor; + while (*local_end != '\0' && *local_end != '@') local_end++; + if (local_end == cursor) return 0; + while (cursor < local_end) { + unsigned char current = *cursor++; + + if (current == '.') { + if (previous_dot || + cursor - 1 == (const unsigned char *)path) return 0; + previous_dot = 1; + continue; + } + previous_dot = 0; + if (current >= 0x80U || isalnum(current) || + strchr("!#$%&'*+-/=?^_`{|}~", current) != NULL) + continue; + return 0; + } + if (previous_dot) return 0; + if (*local_end == '@') { + separator = local_end; + cursor = local_end + 1; + } else { + cursor = local_end; + } + + if (separator == NULL) return *cursor == '\0'; + if (*cursor == '\0' || strchr((const char *)cursor, '@') != NULL) return 0; + return 1; } static int diff --git a/src/agents/smtp/smtpd.c b/src/agents/smtp/smtpd.c index cf055ea..ee29ada 100644 --- a/src/agents/smtp/smtpd.c +++ b/src/agents/smtp/smtpd.c @@ -1582,7 +1582,8 @@ HandleConnection (void *param) if ((ReplyInt = RewriteAddress (Client->nmap.conn, RoutingName, To, sizeof (To))) == MAIL_BOGUS) { - ConnWrite (Client->client.conn, MSG501RECIPNO, MSG501RECIPNO_LEN); + ConnWrite(Client->client.conn, MSG550NOTFOUND, + MSG550NOTFOUND_LEN); } else { if (ReplyInt == MAIL_REMOTE && diff --git a/src/agents/smtp/tests/protocol-test.c b/src/agents/smtp/tests/protocol-test.c index 1160d82..c8fe8aa 100644 --- a/src/agents/smtp/tests/protocol-test.c +++ b/src/agents/smtp/tests/protocol-test.c @@ -143,10 +143,20 @@ TestPaths(void) assert(!SMTPPathIsSafe("user> SIZE=0")); assert(!SMTPPathIsSafe("bad\177@example.test")); assert(SMTPRecipientPathIsValid("user@example.test")); + assert(SMTPRecipientPathIsValid("smtp18-missing-600189@example.test")); assert(SMTPRecipientPathIsValid("m\303\274ller@example.test")); assert(!SMTPRecipientPathIsValid(NULL)); assert(!SMTPRecipientPathIsValid("")); assert(!SMTPRecipientPathIsValid("user name@example.test")); + assert(!SMTPRecipientPathIsValid("@example.test")); + assert(!SMTPRecipientPathIsValid("user@")); + assert(!SMTPRecipientPathIsValid("bad@@example.test")); + assert(!SMTPRecipientPathIsValid(".user@example.test")); + assert(!SMTPRecipientPathIsValid("user.@example.test")); + assert(!SMTPRecipientPathIsValid("user..tag@example.test")); + assert(!SMTPRecipientPathIsValid("\"quoted local\"@example.test")); + assert(!SMTPRecipientPathIsValid("\"quoted@local\"@example.test")); + assert(!SMTPRecipientPathIsValid("\"unterminated@example.test")); } static void