Harden IMAP and POP TLS state handling
Debian Trixie package bundle / packages (push) Successful in 20m44s

This commit is contained in:
Mario Fetka
2026-07-21 10:27:02 +02:00
parent b02dd492eb
commit 12ee80a466
16 changed files with 272 additions and 39 deletions
+4
View File
@@ -37,6 +37,10 @@ ad-hoc downgrade.
AUTH capabilities are generated from the GNU GSASL mechanisms which Bongo
has actually enabled. Password mechanisms are advertised only on a TLS
connection; unencrypted connections offer STARTTLS and keep LOGIN disabled.
STARTTLS rejects arguments and repeated or post-authentication use, discards
already buffered cleartext before the TLS handshake, and closes the connection
without attempting a plaintext IMAP response if negotiation fails. A client
which ignores `LOGINDISABLED` receives a tagged `NO` without a password lookup.
Relevant standards are [RFC 9051](https://www.rfc-editor.org/rfc/rfc9051.html),
[RFC 3501](https://www.rfc-editor.org/rfc/rfc3501.html),
+4
View File
@@ -34,6 +34,10 @@ POP3 implements the normal transaction commands and advertises `TOP`,
`RESP-CODES`, `AUTH-RESP-CODE`, `PIPELINING`, `EXPIRE NEVER`, `UIDL`, and
`UTF8`. STLS is offered before authentication; SASL and password login are
advertised only according to the TLS policy. POP3S provides implicit TLS.
After a successful `UTF8` command, `STLS` is no longer advertised and is
rejected so that the negotiated UTF-8 session state cannot cross into TLS.
Both IMAP STARTTLS and POP3 STLS discard buffered cleartext before negotiating
TLS.
## Server-side filtering
+4 -4
View File
@@ -154,7 +154,7 @@ inputs and outputs is absent from the ZIP.
| ID | Test | R1 | R2 | R3 |
| --- | --- | --- | --- | --- |
| IMAP-01 | Capability list matches implemented rev2/rev1/legacy behaviour | | | |
| IMAP-02 | Port 143 STARTTLS and pre-TLS `LOGINDISABLED` | | | |
| IMAP-02 | Port 143 STARTTLS, pre-TLS `LOGINDISABLED`, tagged `NO` for plaintext LOGIN, repeated STARTTLS rejection | | | |
| IMAP-03 | Port 993 TLS, LOGIN, SASL PLAIN, and SASL LOGIN | | | |
| IMAP-04 | CAPABILITY, NOOP, LOGOUT, ID, and NAMESPACE | | | |
| IMAP-05 | LIST, LSUB, extended LIST, LIST-STATUS, hierarchy quoting | | | |
@@ -171,18 +171,18 @@ inputs and outputs is absent from the ZIP.
| IMAP-16 | ENABLE UTF8=ACCEPT and UTF-8 mailbox/message names | | | |
| IMAP-17 | SPECIAL-USE and CREATE-SPECIAL-USE folders | | | |
| IMAP-18 | Old-client plain IMAP4 subset without rev1/rev2 assumptions | | | |
| IMAP-19 | Invalid states, malformed sets/literals, oversized/pipelined input | | | |
| IMAP-19 | Invalid states, malformed sets/literals, oversized input, and STARTTLS plaintext-pipeline injection resistance | | | |
| IMAP-20 | Reconnect, simultaneous clients, restart, mailbox consistency | | | |
## POP3 and ManageSieve
| ID | Test | R1 | R2 | R3 |
| --- | --- | --- | --- | --- |
| POP-01 | Port 110 CAPA/STLS; credentials unavailable before TLS | | | |
| POP-01 | Port 110 CAPA/STLS; credentials unavailable before TLS; buffered plaintext discarded | | | |
| POP-02 | Port 995 TLS with USER/PASS and shared GSASL mechanisms | | | |
| POP-03 | STAT, LIST, UIDL, RETR, TOP, and dot transparency | | | |
| POP-04 | DELE, RSET, NOOP, QUIT commit, and disconnect rollback | | | |
| POP-05 | UTF8, PIPELINING, response codes, malformed input, timeout | | | |
| POP-05 | UTF8, STLS rejection after UTF8, PIPELINING, response codes, malformed input, timeout | | | |
| SIEVE-01 | Port 4190 greeting, STARTTLS, and GSASL authentication | | | |
| SIEVE-02 | CAPABILITY, HAVESPACE, PUT/CHECK/LIST/GETSCRIPT | | | |
| SIEVE-03 | SETACTIVE, RENAME, DELETE, LOGOUT, and persistence | | | |
+10
View File
@@ -1,6 +1,7 @@
add_executable(bongoimap
imapd.c
capabilities.c
protocol.c
enable.c
utf8-mailbox.c
append-message.c
@@ -46,6 +47,15 @@ if(BUILD_TESTING)
)
add_test(NAME imap-capabilities COMMAND imap-capabilities-test)
add_executable(imap-protocol-test
tests/protocol-test.c
protocol.c
)
target_include_directories(imap-protocol-test PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
)
add_test(NAME imap-protocol COMMAND imap-protocol-test)
add_executable(imap-enable-test
tests/enable-test.c
enable.c
+4
View File
@@ -98,6 +98,8 @@ typedef enum {
STATUS_INVALID_ARGUMENT,
STATUS_INVALID_STATE,
STATUS_UNKNOWN_COMMAND,
STATUS_LOGIN_DISABLED,
STATUS_TLS_UNAVAILABLE,
STATUS_TLS_NEGOTIATION_FAILURE,
STATUS_CHARSET_NOT_SUPPORTED,
STATUS_UID_NOT_FOUND,
@@ -156,6 +158,8 @@ static ImapErrorString ImapErrorStrings[] = {
{ STATUS_INVALID_ARGUMENT, "%s BAD %s invalid arguments\r\n" },
{ STATUS_INVALID_STATE, "%s BAD %s invalid command in current state\r\n" },
{ STATUS_UNKNOWN_COMMAND, "* BAD command unrecognized\r\n" },
{ STATUS_LOGIN_DISABLED, "%s NO %s plaintext LOGIN disabled; use STARTTLS\r\n" },
{ STATUS_TLS_UNAVAILABLE, "%s NO %s TLS unavailable\r\n" },
{ STATUS_TLS_NEGOTIATION_FAILURE, "%s BAD %s negotiation failed\r\n" },
{ 0, NULL }
};
+57
View File
@@ -0,0 +1,57 @@
/****************************************************************************
* <Novell-copyright>
* Copyright (c) 2001 Novell, Inc. All Rights Reserved.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of version 2 of the GNU General Public License
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, contact Novell, Inc.
*
* To contact Novell about this file by physical or electronic mail, you
* may find current contact information at www.novell.com.
* </Novell-copyright>
****************************************************************************/
#include "protocol.h"
#include <ctype.h>
#include <string.h>
static int
EqualsIgnoreCase(const char *left, const char *right)
{
if (!left || !right) return 0;
while (*left && *right) {
if (toupper((unsigned char)*left) != toupper((unsigned char)*right))
return 0;
left++;
right++;
}
return *left == '\0' && *right == '\0';
}
IMAPStartTlsStatus
IMAPStartTlsCheck(const char *command_line, int authenticated,
int tls_active, int tls_available)
{
if (!EqualsIgnoreCase(command_line, "STARTTLS"))
return IMAP_STARTTLS_INVALID_ARGUMENTS;
if (authenticated || tls_active) return IMAP_STARTTLS_INVALID_STATE;
if (!tls_available) return IMAP_STARTTLS_UNAVAILABLE;
return IMAP_STARTTLS_READY;
}
IMAPLoginStatus
IMAPLoginCheck(int authenticated, int tls_active)
{
if (authenticated) return IMAP_LOGIN_INVALID_STATE;
if (!tls_active) return IMAP_LOGIN_DISABLED;
return IMAP_LOGIN_READY;
}
+43
View File
@@ -0,0 +1,43 @@
/****************************************************************************
* <Novell-copyright>
* Copyright (c) 2001 Novell, Inc. All Rights Reserved.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of version 2 of the GNU General Public License
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, contact Novell, Inc.
*
* To contact Novell about this file by physical or electronic mail, you
* may find current contact information at www.novell.com.
* </Novell-copyright>
****************************************************************************/
#ifndef BONGO_IMAP_PROTOCOL_H
#define BONGO_IMAP_PROTOCOL_H
typedef enum {
IMAP_STARTTLS_READY = 0,
IMAP_STARTTLS_INVALID_ARGUMENTS,
IMAP_STARTTLS_INVALID_STATE,
IMAP_STARTTLS_UNAVAILABLE
} IMAPStartTlsStatus;
typedef enum {
IMAP_LOGIN_READY = 0,
IMAP_LOGIN_DISABLED,
IMAP_LOGIN_INVALID_STATE
} IMAPLoginStatus;
IMAPStartTlsStatus IMAPStartTlsCheck(const char *command_line,
int authenticated, int tls_active,
int tls_available);
IMAPLoginStatus IMAPLoginCheck(int authenticated, int tls_active);
#endif
+49 -17
View File
@@ -29,6 +29,7 @@
#include <bongosasl.h>
#include "imapd.h"
#include "protocol.h"
void
ImapAnnotateAliasClient(ImapSession *session)
@@ -298,10 +299,20 @@ ImapCommandLogin(void *param)
{
ImapSession *session = (ImapSession *)param;
long ccode;
IMAPLoginStatus status;
char *username = NULL;
char *password = NULL;
if (session->client.state == STATE_FRESH && session->client.conn->ssl.enable) {
status = IMAPLoginCheck(session->client.state >= STATE_AUTH,
session->client.conn->ssl.enable);
if (status == IMAP_LOGIN_DISABLED)
return(SendError(session->client.conn, session->command.tag, "LOGIN",
STATUS_LOGIN_DISABLED));
if (status == IMAP_LOGIN_INVALID_STATE)
return(SendError(session->client.conn, session->command.tag, "LOGIN",
STATUS_INVALID_STATE));
if (status == IMAP_LOGIN_READY) {
ccode = GrabTwoArguments(session, session->command.buffer + 6, &username, &password);
if (ccode == STATUS_CONTINUE) {
X500Encode(username);
@@ -335,27 +346,48 @@ int
ImapCommandStartTls(void *param)
{
long ccode;
IMAPStartTlsStatus status;
ImapSession *session = (ImapSession *)param;
if (session->client.state < STATE_AUTH) {
if (Imap.server.ssl.config.options) {
if (((ccode = ConnWriteF(session->client.conn, "%s OK Begin TLS negotiation\r\n",session->command.tag)) != -1) && (ccode = ConnFlush(session->client.conn))) {
session->client.conn->ssl.enable = TRUE;
if (ConnNegotiate(session->client.conn, Imap.server.ssl.context)) {
return(STATUS_CONTINUE);
}
status = IMAPStartTlsCheck(session->command.buffer,
session->client.state >= STATE_AUTH,
session->client.conn->ssl.enable,
Imap.server.ssl.context != NULL);
if (status == IMAP_STARTTLS_INVALID_ARGUMENTS)
return(SendError(session->client.conn, session->command.tag,
"STARTTLS", STATUS_INVALID_ARGUMENT));
if (status == IMAP_STARTTLS_INVALID_STATE)
return(SendError(session->client.conn, session->command.tag,
"STARTTLS", STATUS_INVALID_STATE));
if (status == IMAP_STARTTLS_UNAVAILABLE)
return(SendError(session->client.conn, session->command.tag,
"STARTTLS", STATUS_TLS_UNAVAILABLE));
return(SendError(session->client.conn, session->command.tag, "STARTTLS", STATUS_TLS_NEGOTIATION_FAILURE));
}
ccode = ConnWriteF(session->client.conn,
"%s OK Begin TLS negotiation\r\n",
session->command.tag);
if (ccode == -1 || ConnFlush(session->client.conn) == -1)
return(STATUS_ABORT);
return(STATUS_ABORT);
}
return(SendError(session->client.conn, session->command.tag, "", STATUS_UNKNOWN_COMMAND));
}
return(SendError(session->client.conn, session->command.tag, "STARTTLS", STATUS_INVALID_STATE));
/*
* Never carry pipelined cleartext across STARTTLS. RFC 9051 requires
* extra bytes already received after the command to become TLS input or
* to be discarded. Discarding them avoids plaintext command injection
* and matches the POP3 STLS path.
*/
session->client.conn->receive.read =
session->client.conn->receive.write =
session->client.conn->receive.buffer;
session->client.conn->receive.remaining = CONN_TCP_MTU;
session->client.conn->receive.buffer[0] = '\0';
session->client.conn->ssl.enable = TRUE;
session->client.enabledCapabilities = 0;
if (ConnNegotiate(session->client.conn, Imap.server.ssl.context))
return(STATUS_CONTINUE);
/* TLS bytes may already have been exchanged; no IMAP response is safe. */
ConnClose(session->client.conn);
return(STATUS_ABORT);
}
+46
View File
@@ -0,0 +1,46 @@
/****************************************************************************
* <Novell-copyright>
* Copyright (c) 2001 Novell, Inc. All Rights Reserved.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of version 2 of the GNU General Public License
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, contact Novell, Inc.
*
* To contact Novell about this file by physical or electronic mail, you
* may find current contact information at www.novell.com.
* </Novell-copyright>
****************************************************************************/
#include "protocol.h"
#include <assert.h>
int
main(void)
{
assert(IMAPStartTlsCheck("STARTTLS", 0, 0, 1) == IMAP_STARTTLS_READY);
assert(IMAPStartTlsCheck("starttls", 0, 0, 1) == IMAP_STARTTLS_READY);
assert(IMAPStartTlsCheck("STARTTLS ", 0, 0, 1) ==
IMAP_STARTTLS_INVALID_ARGUMENTS);
assert(IMAPStartTlsCheck("STARTTLS extra", 0, 0, 1) ==
IMAP_STARTTLS_INVALID_ARGUMENTS);
assert(IMAPStartTlsCheck("STARTTLS", 1, 0, 1) ==
IMAP_STARTTLS_INVALID_STATE);
assert(IMAPStartTlsCheck("STARTTLS", 0, 1, 1) ==
IMAP_STARTTLS_INVALID_STATE);
assert(IMAPStartTlsCheck("STARTTLS", 0, 0, 0) ==
IMAP_STARTTLS_UNAVAILABLE);
assert(IMAPLoginCheck(0, 1) == IMAP_LOGIN_READY);
assert(IMAPLoginCheck(0, 0) == IMAP_LOGIN_DISABLED);
assert(IMAPLoginCheck(1, 1) == IMAP_LOGIN_INVALID_STATE);
assert(IMAPLoginCheck(1, 0) == IMAP_LOGIN_INVALID_STATE);
return 0;
}
+3 -1
View File
@@ -47,6 +47,7 @@ Append(char *buffer, size_t buffer_size, size_t *length,
size_t
POP3FormatCapabilities(char *buffer, size_t buffer_size,
int authorization_state, int tls_active,
int utf8_active,
int stls_available,
const char *sasl_mechanisms)
{
@@ -63,7 +64,8 @@ POP3FormatCapabilities(char *buffer, size_t buffer_size,
sasl_mechanisms)) ||
(authorization_state && tls_active &&
!Append(buffer, buffer_size, &length, "USER\r\n")) ||
(authorization_state && !tls_active && stls_available &&
(authorization_state && !tls_active && !utf8_active &&
stls_available &&
!Append(buffer, buffer_size, &length, "STLS\r\n")) ||
!Append(buffer, buffer_size, &length, ".\r\n"))
return 0;
+1
View File
@@ -27,6 +27,7 @@
/* Returns the number of bytes written, or 0 if the buffer was too small. */
size_t POP3FormatCapabilities(char *buffer, size_t buffer_size,
int authorization_state, int tls_active,
int utf8_active,
int stls_available,
const char *sasl_mechanisms);
+13 -12
View File
@@ -1136,7 +1136,7 @@ POP3CommandCapa(void *param)
}
capabilityLength = POP3FormatCapabilities(
client->buffer, sizeof(client->buffer), authorizationState,
client->conn->ssl.enable,
client->conn->ssl.enable, client->utf8Mode,
POP3.server.ssl.enable && POP3.server.ssl.context != NULL, mechanisms);
ccode = capabilityLength
? ConnWrite(client->conn, client->buffer, capabilityLength)
@@ -1533,6 +1533,7 @@ POP3CommandSTLS(void *param)
{
int ccode;
BOOL result;
POP3STLSStatus status;
POP3Client *client = (POP3Client *)param;
if (!POP3ArgumentsEmpty(client->command +
@@ -1544,19 +1545,19 @@ POP3CommandSTLS(void *param)
return ConnWrite(client->conn, MSGERRSYNTAX,
sizeof(MSGERRSYNTAX) - 1);
if ((client->state == POP3_CLIENT_FRESH ||
client->state == POP3_CLIENT_AUTHORIZATION) &&
!client->conn->ssl.enable) {
if (POP3.server.ssl.enable && POP3.server.ssl.context) {
ccode = ConnWrite(client->conn, MSGOKSTARTTLS, sizeof(MSGOKSTARTTLS) - 1);
} else {
return ConnWrite(client->conn,
"-ERR [SYS/PERM] TLS unavailable\r\n",
sizeof("-ERR [SYS/PERM] TLS unavailable\r\n") - 1);
}
} else {
status = POP3STLSCheck(client->state == POP3_CLIENT_FRESH ||
client->state == POP3_CLIENT_AUTHORIZATION,
client->conn->ssl.enable, client->utf8Mode,
POP3.server.ssl.enable && POP3.server.ssl.context);
if (status == POP3_STLS_INVALID_STATE) {
return(ConnWrite(client->conn, MSGERRBADSTATE, sizeof(MSGERRBADSTATE) - 1));
}
if (status == POP3_STLS_UNAVAILABLE)
return ConnWrite(client->conn,
"-ERR [SYS/PERM] TLS unavailable\r\n",
sizeof("-ERR [SYS/PERM] TLS unavailable\r\n") - 1);
ccode = ConnWrite(client->conn, MSGOKSTARTTLS, sizeof(MSGOKSTARTTLS) - 1);
if (ccode != -1) {
ccode = ConnFlush(client->conn);
+10
View File
@@ -57,6 +57,16 @@ POP3ArgumentsEmpty(const char *arguments)
return arguments && *arguments == '\0';
}
POP3STLSStatus
POP3STLSCheck(int authorization_state, int tls_active, int utf8_active,
int tls_available)
{
if (!authorization_state || tls_active || utf8_active)
return POP3_STLS_INVALID_STATE;
if (!tls_available) return POP3_STLS_UNAVAILABLE;
return POP3_STLS_READY;
}
int
POP3ParseMessageNumber(const char *arguments, uint32_t *message_number)
{
+8
View File
@@ -35,10 +35,18 @@ typedef struct {
int ends_in_cr;
} POP3MultilineWriter;
typedef enum {
POP3_STLS_READY = 0,
POP3_STLS_INVALID_STATE,
POP3_STLS_UNAVAILABLE
} POP3STLSStatus;
int POP3Contains8Bit(const void *data, size_t length);
int POP3UTF8Valid(const void *data, size_t length);
int POP3ArgumentsEmpty(const char *arguments);
POP3STLSStatus POP3STLSCheck(int authorization_state, int tls_active,
int utf8_active, int tls_available);
int POP3ParseMessageNumber(const char *arguments, uint32_t *message_number);
int POP3ParseTopArguments(const char *arguments, uint32_t *message_number,
uint32_t *line_count);
+11 -5
View File
@@ -30,29 +30,35 @@ main(void)
char buffer[1024];
char small[8];
assert(POP3FormatCapabilities(buffer, sizeof(buffer), 1, 0, 0, NULL) > 0);
assert(POP3FormatCapabilities(buffer, sizeof(buffer), 1, 0, 0, 0,
NULL) > 0);
assert(!strstr(buffer, "STLS"));
assert(!strstr(buffer, "SASL"));
assert(!strstr(buffer, "USER"));
assert(strstr(buffer, "UTF8\r\n"));
assert(POP3FormatCapabilities(buffer, sizeof(buffer), 1, 0, 1, NULL) > 0);
assert(POP3FormatCapabilities(buffer, sizeof(buffer), 1, 0, 0, 1,
NULL) > 0);
assert(strstr(buffer, "STLS\r\n"));
assert(!strstr(buffer, "SASL"));
assert(POP3FormatCapabilities(buffer, sizeof(buffer), 1, 1, 1,
assert(POP3FormatCapabilities(buffer, sizeof(buffer), 1, 1, 0, 1,
"PLAIN LOGIN") > 0);
assert(!strstr(buffer, "STLS"));
assert(strstr(buffer, "SASL PLAIN LOGIN\r\n"));
assert(strstr(buffer, "USER\r\n"));
assert(POP3FormatCapabilities(buffer, sizeof(buffer), 0, 1, 1,
assert(POP3FormatCapabilities(buffer, sizeof(buffer), 0, 1, 0, 1,
"PLAIN LOGIN") > 0);
assert(!strstr(buffer, "STLS"));
assert(strstr(buffer, "SASL PLAIN LOGIN\r\n"));
assert(!strstr(buffer, "USER"));
assert(POP3FormatCapabilities(small, sizeof(small), 1, 1, 1,
assert(POP3FormatCapabilities(buffer, sizeof(buffer), 1, 0, 1, 1,
NULL) > 0);
assert(!strstr(buffer, "STLS"));
assert(POP3FormatCapabilities(small, sizeof(small), 1, 1, 0, 1,
"PLAIN LOGIN") == 0);
assert(small[0] == '\0');
return 0;
+5
View File
@@ -49,6 +49,11 @@ TestArguments(void)
assert(POP3ArgumentsEmpty(""));
assert(!POP3ArgumentsEmpty(" "));
assert(POP3STLSCheck(1, 0, 0, 1) == POP3_STLS_READY);
assert(POP3STLSCheck(0, 0, 0, 1) == POP3_STLS_INVALID_STATE);
assert(POP3STLSCheck(1, 1, 0, 1) == POP3_STLS_INVALID_STATE);
assert(POP3STLSCheck(1, 0, 1, 1) == POP3_STLS_INVALID_STATE);
assert(POP3STLSCheck(1, 0, 0, 0) == POP3_STLS_UNAVAILABLE);
assert(POP3ParseMessageNumber(" 1", &message) && message == 1);
assert(POP3ParseMessageNumber("\t4294967295", &message) &&
message == UINT32_MAX);