Skip to content

Commit b5d4869

Browse files
committed
net_smtp: Prevent SMTP smuggling attacks.
Reject messages containing a bare CR or LF anywhere, to avoid being vulnerable to SMTP smuggling attacks. These could occur if an outbound mail server ignores bare CR or LF and an inbound server tolerates (and interprets) them. Previously, we ignored them, so while not vulnerable on the inbound side, we were vulnerable on the outbound side, i.e. could have been tricked into sending a malformed message with a stuffed SMTP transaction to a vulnerable inbound server, which would then process the stuffed message that appeared to legitimately originate from us. Explicitly rejecting (rather than ignoring) bare line endings prevents this.
1 parent c084704 commit b5d4869

File tree

4 files changed

+66
-6
lines changed

4 files changed

+66
-6
lines changed

bbs/string.c

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,17 @@ int bbs_term_line(char *restrict c)
263263
return len;
264264
}
265265

266+
int bbs_str_contains_line_ending(const char *s)
267+
{
268+
while (*s) {
269+
if (*s == '\r' || *s == '\n') {
270+
return *s;
271+
}
272+
s++;
273+
}
274+
return 0;
275+
}
276+
266277
int bbs_str_contains_bare_lf(const char *s)
267278
{
268279
int prev_cr = 0;

include/string.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,14 @@ int bbs_strncount(const char *restrict s, size_t len, char c) __attribute__ ((pu
8787
*/
8888
int bbs_term_line(char *restrict c);
8989

90+
/*!
91+
* \brief Whether a string contains a CR or LF character
92+
* \param s A NUL-terminated string
93+
* \retval '\r' if CR present, '\n' if LF present, 0 if neither present
94+
* \note If both are present, only the first one encountered is returned
95+
*/
96+
int bbs_str_contains_line_ending(const char *s);
97+
9098
/*!
9199
* \brief Whether a NUL-terminated string contains bare line feeds (LF characters not immediately preceded by a CR)
92100
* \param s String to search

nets/net_smtp.c

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3260,6 +3260,7 @@ static int handle_data(struct smtp_session *smtp, char *s, struct readline_data
32603260
FILE *fp;
32613261
char template[256];
32623262
int datafail = 0;
3263+
int bare_cr_lf = 0;
32633264
int indataheaders = 1;
32643265

32653266
REQUIRE_HELO();
@@ -3279,6 +3280,7 @@ static int handle_data(struct smtp_session *smtp, char *s, struct readline_data
32793280

32803281
for (;;) {
32813282
size_t len;
3283+
int crlfres;
32823284
ssize_t res = bbs_node_readline(smtp->node, rldata, "\r\n", MIN_MS(3)); /* RFC 5321 4.5.3.2.5 */
32833285
if (res < 0) {
32843286
bbs_delete_file(template);
@@ -3291,7 +3293,7 @@ static int handle_data(struct smtp_session *smtp, char *s, struct readline_data
32913293
* In practice, most messages that violate this seem to be spam anyways,
32923294
* so we have no reason to tolerate noncompliant messages.
32933295
* However, we need to reject it properly with the right error message before disconnecting. */
3294-
smtp_reply_nostatus(smtp, 550, "Maximum line length exceeded");
3296+
smtp_reply_nostatus(smtp, 550, "Maximum line length exceeded (violates RFC 5322 2.3)");
32953297
smtp->failures += 3; /* Semantically, this is a bad client. However, we're just going to disconnect now anyways, so this doesn't really matter. */
32963298
}
32973299
return -1;
@@ -3307,6 +3309,10 @@ static int handle_data(struct smtp_session *smtp, char *s, struct readline_data
33073309
if (smtp->tflags.datalen >= smtp_max_session_message_size(smtp)) {
33083310
/* Message too large. */
33093311
smtp_reply(smtp, 552, 5.2.3, "Your message exceeded our message size limits");
3312+
} else if (bare_cr_lf) {
3313+
smtp_reply(smtp, 554, 5.0.0, "Message rejected, bare %s detected (violates RFC 5322 2.3)", bare_cr_lf == '\r' ? "CR" : "LF");
3314+
smtp->failures += 2;
3315+
return 0;
33103316
} else {
33113317
/* Message not successfully received in totality, so reject it. */
33123318
smtp_reply(smtp, 451, 4.3.0, "Message not received successfully, try again");
@@ -3351,6 +3357,23 @@ static int handle_data(struct smtp_session *smtp, char *s, struct readline_data
33513357
continue; /* Corruption already happened, just ignore the rest of the message for now. */
33523358
}
33533359

3360+
crlfres = bbs_str_contains_line_ending(rldata->buf);
3361+
if (crlfres) {
3362+
/* The normal CR LF line ending will be consumed by bbs_node_readline.
3363+
* Therefore, there should NOT be any bare CR or LF on its own.
3364+
* Not only is it not RFC-compliant, but it would also open us up
3365+
* to SMTP smuggling attacks where we could inadvertently relay
3366+
* messages to another server that we did not intend to, which happens
3367+
* when the outbound server ignores bare CR or LF and the inbound
3368+
* server interprets them, allowing for a separate transaction to
3369+
* be "stuffed" on the outbound side (see https://smtpsmuggling.com/).
3370+
* By strictly rejecting such messages, we are not vulnerable as either
3371+
* the outbound or inbound server. */
3372+
bare_cr_lf = crlfres;
3373+
datafail = 1;
3374+
continue;
3375+
}
3376+
33543377
if (indataheaders) {
33553378
if ((smtp->fromlocal || smtp->msa) && STARTS_WITH(s, "From:")) {
33563379
const char *newfromhdraddr = S_IF(s + 5);

tests/test_smtp_mta.c

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,6 @@ static int run(void)
138138
/* Ensure mail loops are prevented */
139139
SWRITE(clientfd, "RSET" ENDL);
140140
CLIENT_EXPECT(clientfd, "250");
141-
SWRITE(clientfd, "EHLO " TEST_EXTERNAL_DOMAIN ENDL);
142-
CLIENT_EXPECT_EVENTUALLY(clientfd, "250 ");
143141
SWRITE(clientfd, "MAIL FROM:<" TEST_EMAIL_EXTERNAL ">\r\n");
144142
CLIENT_EXPECT(clientfd, "250");
145143
SWRITE(clientfd, "RCPT TO:<" TEST_EMAIL ">\r\n");
@@ -155,7 +153,7 @@ static int run(void)
155153
SWRITE(clientfd, "." ENDL); /* EOM */
156154
CLIENT_EXPECT(clientfd, "554"); /* Mail loop detected */
157155

158-
/* Test messages that are exactly as long as the readline buffer. */
156+
/* Test message with a bare line feed. Should be rejected. */
159157
SWRITE(clientfd, "RSET" ENDL);
160158
CLIENT_EXPECT(clientfd, "250");
161159
SWRITE(clientfd, "EHLO " TEST_EXTERNAL_DOMAIN ENDL);
@@ -166,6 +164,28 @@ static int run(void)
166164
CLIENT_EXPECT(clientfd, "250");
167165
SWRITE(clientfd, "DATA\r\n");
168166
CLIENT_EXPECT(clientfd, "354");
167+
SWRITE(clientfd, "Subject: Test\r\n"
168+
"\r\n"
169+
"Hello there\n, this is a malformed message\r\n"
170+
"The end.\r\n");
171+
SWRITE(clientfd, "." ENDL); /* EOM */
172+
CLIENT_EXPECT(clientfd, "554");
173+
174+
/* Reconnect since we'd now get tarpitted */
175+
close(clientfd);
176+
clientfd = test_make_socket(25);
177+
REQUIRE_FD(clientfd);
178+
CLIENT_EXPECT_EVENTUALLY(clientfd, "220 ");
179+
SWRITE(clientfd, "EHLO " TEST_EXTERNAL_DOMAIN ENDL);
180+
CLIENT_EXPECT_EVENTUALLY(clientfd, "250 ");
181+
182+
/* Test messages that are exactly as long as the readline buffer. */
183+
SWRITE(clientfd, "MAIL FROM:<" TEST_EMAIL_EXTERNAL ">\r\n");
184+
CLIENT_EXPECT(clientfd, "250");
185+
SWRITE(clientfd, "RCPT TO:<" TEST_EMAIL ">\r\n");
186+
CLIENT_EXPECT(clientfd, "250");
187+
SWRITE(clientfd, "DATA\r\n");
188+
CLIENT_EXPECT(clientfd, "354");
169189
SWRITE(clientfd, "Subject: Test\r\n"
170190
"Content-Type: text/plain; charset=utf-8; format=flowed\r\n"
171191
"Content-Transfer-Encoding: 8bit\r\n"
@@ -196,8 +216,6 @@ static int run(void)
196216
/* Ensure messages with lines over 1,000 characters are rejected */
197217
SWRITE(clientfd, "RSET" ENDL);
198218
CLIENT_EXPECT(clientfd, "250");
199-
SWRITE(clientfd, "EHLO " TEST_EXTERNAL_DOMAIN ENDL);
200-
CLIENT_EXPECT_EVENTUALLY(clientfd, "250 ");
201219
SWRITE(clientfd, "MAIL FROM:<" TEST_EMAIL_EXTERNAL ">\r\n");
202220
CLIENT_EXPECT(clientfd, "250");
203221
SWRITE(clientfd, "RCPT TO:<" TEST_EMAIL ">\r\n");

0 commit comments

Comments
 (0)