[packages/dovecot] Backport security fixes from 2.4.x; rel 9
arekm
arekm at pld-linux.org
Wed Jul 29 21:31:36 CEST 2026
commit 1ca57950ecb751388a27eaf5a5c83db4d89c4fac
Author: Arkadiusz Miśkiewicz <arekm at maven.pl>
Date: Wed Jul 29 20:42:50 2026 +0200
Backport security fixes from 2.4.x; rel 9
CVE-2025-59031 CVE-2026-0394 CVE-2026-27855 CVE-2026-27856 CVE-2026-27857
CVE-2026-27859 CVE-2026-33603 CVE-2026-40020 CVE-2026-42006; 2.3 is EOL
upstream, so patches come from the Debian/Ubuntu 2.3 backports.
CVE-2025-59031.patch | 134 +++++++++++++
CVE-2026-0394.patch | 121 ++++++++++++
CVE-2026-27855.patch | 171 ++++++++++++++++
CVE-2026-27856.patch | 120 ++++++++++++
CVE-2026-27857.patch | 539 +++++++++++++++++++++++++++++++++++++++++++++++++++
CVE-2026-27859.patch | 98 ++++++++++
CVE-2026-33603.patch | 37 ++++
CVE-2026-40020.patch | 129 ++++++++++++
CVE-2026-42006.patch | 99 ++++++++++
dovecot.spec | 22 ++-
10 files changed, 1468 insertions(+), 2 deletions(-)
---
diff --git a/dovecot.spec b/dovecot.spec
index b8b2444..e9a1206 100644
--- a/dovecot.spec
+++ b/dovecot.spec
@@ -13,7 +13,7 @@ Summary: IMAP and POP3 server written with security primarily in mind
Summary(pl.UTF-8): Serwer IMAP i POP3 pisany głównie z myślą o bezpieczeństwie
Name: dovecot
Version: 2.3.21.1
-Release: 8
+Release: 9
Epoch: 1
License: MIT (libraries), LGPL v2.1 (the rest)
Group: Networking/Daemons
@@ -31,6 +31,16 @@ Patch4: openssl3.patch
Patch5: icu76.patch
Patch6: dovecot-lua-export-on-auth-fail.patch
Patch7: tests.patch
+# 2.3 is EOL upstream; security fixes backported from 2.4.x via Debian/Ubuntu
+Patch8: CVE-2025-59031.patch
+Patch9: CVE-2026-0394.patch
+Patch10: CVE-2026-27855.patch
+Patch11: CVE-2026-27856.patch
+Patch12: CVE-2026-27857.patch
+Patch13: CVE-2026-27859.patch
+Patch14: CVE-2026-33603.patch
+Patch15: CVE-2026-40020.patch
+Patch16: CVE-2026-42006.patch
URL: http://dovecot.org/
BuildRequires: autoconf
BuildRequires: automake
@@ -177,6 +187,15 @@ Pakiet programistyczny do tworzenia wtyczek dla Dovecota.
%patch -P5 -p1
%patch -P6 -p1
%patch -P7 -p1
+%patch -P8 -p1
+%patch -P9 -p1
+%patch -P10 -p1
+%patch -P11 -p1
+%patch -P12 -p1
+%patch -P13 -p1
+%patch -P14 -p1
+%patch -P15 -p1
+%patch -P16 -p1
%{__sed} -i 's,/usr/lib/dovecot,%{_libdir}/dovecot,g' doc/example-config/*.conf doc/example-config/conf.d/*.conf
@@ -342,7 +361,6 @@ fi
%attr(755,root,root) %{_libexecdir}/%{name}/auth
%attr(755,root,root) %{_libexecdir}/%{name}/checkpassword-reply
%attr(755,root,root) %{_libexecdir}/%{name}/config
-%attr(755,root,root) %{_libexecdir}/%{name}/decode2text.sh
%attr(755,root,root) %{_libexecdir}/%{name}/deliver
%attr(755,root,root) %{_libexecdir}/%{name}/dict
%attr(755,root,root) %{_libexecdir}/%{name}/director
diff --git a/CVE-2025-59031.patch b/CVE-2025-59031.patch
new file mode 100644
index 0000000..a25786a
--- /dev/null
+++ b/CVE-2025-59031.patch
@@ -0,0 +1,134 @@
+From 089edc7750160bf224011c015347db1bdea435e3 Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi at open-xchange.com>
+Date: Thu, 8 Jan 2026 08:51:59 +0200
+Subject: [PATCH 02/24] fts: Remove decode2text.sh
+
+The script is flawed and not fit for production use, should
+recommend writing your own script, or using Apache Tika.
+---
+ src/plugins/fts/Makefile.am | 3 -
+ src/plugins/fts/decode2text.sh | 105 ---------------------------------
+ 2 files changed, 108 deletions(-)
+ delete mode 100755 src/plugins/fts/decode2text.sh
+
+--- a/src/plugins/fts/Makefile.am
++++ b/src/plugins/fts/Makefile.am
+@@ -63,9 +63,6 @@ xml2text_CPPFLAGS = $(AM_CPPFLAGS) $(BIN
+ xml2text_LDADD = $(LIBDOVECOT) $(BINARY_LDFLAGS)
+ xml2text_DEPENDENCIES = $(module_LTLIBRARIES) $(LIBDOVECOT_DEPS)
+
+-pkglibexec_SCRIPTS = decode2text.sh
+-EXTRA_DIST = $(pkglibexec_SCRIPTS)
+-
+ doveadm_module_LTLIBRARIES = \
+ lib20_doveadm_fts_plugin.la
+
+--- a/src/plugins/fts/decode2text.sh
++++ b//dev/null
+@@ -1,105 +0,0 @@
+-#!/bin/sh
+-
+-# Example attachment decoder script. The attachment comes from stdin, and
+-# the script is expected to output UTF-8 data to stdout. (If the output isn't
+-# UTF-8, everything except valid UTF-8 sequences are dropped from it.)
+-
+-# The attachment decoding is enabled by setting:
+-#
+-# plugin {
+-# fts_decoder = decode2text
+-# }
+-# service decode2text {
+-# executable = script /usr/local/libexec/dovecot/decode2text.sh
+-# user = dovecot
+-# unix_listener decode2text {
+-# mode = 0666
+-# }
+-# }
+-
+-libexec_dir=`dirname $0`
+-content_type=$1
+-
+-# The second parameter is the format's filename extension, which is used when
+-# found from a filename of application/octet-stream. You can also add more
+-# extensions by giving more parameters.
+-formats='application/pdf pdf
+-application/x-pdf pdf
+-application/msword doc
+-application/mspowerpoint ppt
+-application/vnd.ms-powerpoint ppt
+-application/ms-excel xls
+-application/x-msexcel xls
+-application/vnd.ms-excel xls
+-application/vnd.openxmlformats-officedocument.wordprocessingml.document docx
+-application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx
+-application/vnd.openxmlformats-officedocument.presentationml.presentation pptx
+-application/vnd.oasis.opendocument.text odt
+-application/vnd.oasis.opendocument.spreadsheet ods
+-application/vnd.oasis.opendocument.presentation odp
+-'
+-
+-if [ "$content_type" = "" ]; then
+- echo "$formats"
+- exit 0
+-fi
+-
+-fmt=`echo "$formats" | grep -w "^$content_type" | cut -d ' ' -f 2`
+-if [ "$fmt" = "" ]; then
+- echo "Content-Type: $content_type not supported" >&2
+- exit 1
+-fi
+-
+-# most decoders can't handle stdin directly, so write the attachment
+-# to a temp file
+-path=`mktemp`
+-trap "rm -f $path" 0 1 2 3 14 15
+-cat > $path
+-
+-xmlunzip() {
+- name=$1
+-
+- tempdir=`mktemp -d`
+- if [ "$tempdir" = "" ]; then
+- exit 1
+- fi
+- trap "rm -rf $path $tempdir" 0 1 2 3 14 15
+- cd $tempdir || exit 1
+- unzip -q "$path" 2>/dev/null || exit 0
+- find . -name "$name" -print0 | xargs -0 cat |
+- $libexec_dir/xml2text
+-}
+-
+-wait_timeout() {
+- childpid=$!
+- trap "kill -9 $childpid; rm -f $path" 1 2 3 14 15
+- wait $childpid
+-}
+-
+-LANG=en_US.UTF-8
+-export LANG
+-if [ $fmt = "pdf" ]; then
+- /usr/bin/pdftotext $path - 2>/dev/null&
+- wait_timeout 2>/dev/null
+-elif [ $fmt = "doc" ]; then
+- (/usr/bin/catdoc $path; true) 2>/dev/null&
+- wait_timeout 2>/dev/null
+-elif [ $fmt = "ppt" ]; then
+- (/usr/bin/catppt $path; true) 2>/dev/null&
+- wait_timeout 2>/dev/null
+-elif [ $fmt = "xls" ]; then
+- (/usr/bin/xls2csv $path; true) 2>/dev/null&
+- wait_timeout 2>/dev/null
+-elif [ $fmt = "odt" -o $fmt = "ods" -o $fmt = "odp" ]; then
+- xmlunzip "content.xml"
+-elif [ $fmt = "docx" ]; then
+- xmlunzip "document.xml"
+-elif [ $fmt = "xlsx" ]; then
+- xmlunzip "sharedStrings.xml"
+-elif [ $fmt = "pptx" ]; then
+- xmlunzip "slide*.xml"
+-else
+- echo "Buggy decoder script: $fmt not handled" >&2
+- exit 1
+-fi
+-exit 0
+
diff --git a/CVE-2026-0394.patch b/CVE-2026-0394.patch
new file mode 100644
index 0000000..9f71d9c
--- /dev/null
+++ b/CVE-2026-0394.patch
@@ -0,0 +1,121 @@
+From c4fbf9a46ebabb7a580087033ee1b841e52d905e Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi at open-xchange.com>
+Date: Wed, 13 Nov 2024 11:38:30 +0200
+Subject: [PATCH] auth: db-passwd-file - Add db_passwd_fix_path()
+
+---
+ src/auth/db-passwd-file.c | 25 +++++++++++++++++++++++++
+ src/auth/db-passwd-file.h | 2 ++
+ 2 files changed, 27 insertions(+)
+
+--- a/src/auth/db-passwd-file.c
++++ b/src/auth/db-passwd-file.c
+@@ -14,6 +14,7 @@
+ #include "str.h"
+ #include "eacces-error.h"
+ #include "ioloop.h"
++#include "path-util.h"
+
+ #include <unistd.h>
+ #include <fcntl.h>
+@@ -420,6 +421,30 @@ void db_passwd_file_unref(struct db_pass
+ i_free(db);
+ }
+
++int db_passwd_fix_path(const char *path, const char **path_r,
++ const char *orig_path, const char **error_r)
++{
++ /* normalize path */
++ const char *normalized;
++ if (t_normpath(path, &normalized, error_r) < 0)
++ return -1;
++
++ /* check base path */
++ const char *p;
++ if (*orig_path != '%' &&
++ (p = strchr(orig_path, '%')) != NULL) {
++ ptrdiff_t len = p - orig_path;
++ if (strncmp(orig_path, normalized, len) != 0) {
++ *error_r = t_strdup_printf("Path is outside '%s'",
++ t_strdup_until(orig_path, p));
++ return -1;
++ }
++ }
++
++ *path_r = normalized;
++ return 0;
++}
++
+ static const char *
+ path_fix(const char *path,
+ const struct auth_request *auth_request ATTR_UNUSED)
+--- a/src/auth/db-passwd-file.h
++++ b/src/auth/db-passwd-file.h
+@@ -45,6 +45,8 @@ struct db_passwd_file {
+ bool userdb_warn_missing:1;
+ };
+
++int db_passwd_fix_path(const char *path, const char **path_r,
++ const char *orig_path, const char **error_r);
+ int db_passwd_file_lookup(struct db_passwd_file *db,
+ struct auth_request *request,
+ const char *username_format,
+
+From 7fb773cffa3d78b587c406ebfeaa5a1e911a1835 Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi at open-xchange.com>
+Date: Wed, 13 Nov 2024 11:42:05 +0200
+Subject: [PATCH] auth: db-passwd-file - Normalize path with
+ db_passwd_fix_path()
+
+Otherwise there is a chance that we leak one ../
+---
+ src/auth/db-passwd-file.c | 22 ++++++----------------
+ 1 file changed, 6 insertions(+), 16 deletions(-)
+
+--- a/src/auth/db-passwd-file.c
++++ b/src/auth/db-passwd-file.c
+@@ -445,21 +445,6 @@ int db_passwd_fix_path(const char *path,
+ return 0;
+ }
+
+-static const char *
+-path_fix(const char *path,
+- const struct auth_request *auth_request ATTR_UNUSED)
+-{
+- const char *p;
+-
+- p = strchr(path, '/');
+- if (p == NULL)
+- return path;
+-
+- /* most likely this is an invalid request. just cut off the '/' and
+- everything after it. */
+- return t_strdup_until(path, p);
+-}
+-
+ int db_passwd_file_lookup(struct db_passwd_file *db,
+ struct auth_request *request,
+ const char *username_format,
+@@ -474,14 +459,19 @@ int db_passwd_file_lookup(struct db_pass
+ pw = db->default_file;
+ else {
+ dest = t_str_new(256);
+- if (auth_request_var_expand(dest, db->path, request, path_fix,
++ if (auth_request_var_expand(dest, db->path, request, NULL,
+ &error) <= 0) {
+ e_error(authdb_event(request),
+ "Failed to expand passwd-file path %s: %s",
+ db->path, error);
+ return -1;
+ }
+-
++ const char *path;
++ if (db_passwd_fix_path(str_c(dest), &path, db->path, &error) < 0) {
++ e_info(authdb_event(request), "Failed to normalize path: %s",
++ error);
++ return 0;
++ }
+ pw = hash_table_lookup(db->files, str_c(dest));
+ if (pw == NULL) {
+ /* doesn't exist yet. create lookup for it. */
+
diff --git a/CVE-2026-27855.patch b/CVE-2026-27855.patch
new file mode 100644
index 0000000..dadcebf
--- /dev/null
+++ b/CVE-2026-27855.patch
@@ -0,0 +1,171 @@
+From f1bc3ea8ba747fcfe14ab56685d400e4e3cff130 Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi at open-xchange.com>
+Date: Mon, 9 Mar 2026 20:04:27 +0200
+Subject: [PATCH 21/24] auth: cache - Use translated username in
+ auth_cache_remove()
+
+---
+ src/auth/auth-cache.c | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+--- a/src/auth/auth-cache.c
++++ b/src/auth/auth-cache.c
+@@ -472,7 +472,7 @@ void auth_cache_remove(struct auth_cache
+ {
+ struct auth_cache_node *node;
+
+- key = auth_request_expand_cache_key(request, key, request->fields.user);
++ key = auth_request_expand_cache_key(request, key, request->fields.translated_username);
+ node = hash_table_lookup(cache->hash, key);
+ if (node == NULL)
+ return;
+
+From f2119a25a439f599b0a30fc674300b8f354e5b67 Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi at open-xchange.com>
+Date: Wed, 11 Mar 2026 12:30:32 +0200
+Subject: [PATCH 22/24] auth: Move passdb event lifecycle handling to
+ auth_request_passdb_event_(begin|end)
+
+---
+ src/auth/auth-request.c | 39 +++++++++++++++++++++++++++------------
+ src/auth/auth-request.h | 3 +++
+ 2 files changed, 30 insertions(+), 12 deletions(-)
+
+--- a/src/auth/auth-request.c
++++ b/src/auth/auth-request.c
+@@ -628,16 +628,11 @@ auth_request_cache_result_to_str(enum au
+ }
+ }
+
+-void auth_request_passdb_lookup_begin(struct auth_request *request)
++void auth_request_passdb_event_begin(struct auth_request *request)
+ {
+ struct event *event;
+ const char *name;
+
+- i_assert(request->passdb != NULL);
+- i_assert(!request->userdb_lookup);
+-
+- request->passdb_cache_result = AUTH_REQUEST_CACHE_NONE;
+-
+ name = (request->passdb->set->name[0] != '\0' ?
+ request->passdb->set->name :
+ request->passdb->passdb->iface.name);
+@@ -655,11 +650,35 @@ void auth_request_passdb_lookup_begin(st
+ else if (*request->passdb->set->auth_verbose == 'n')
+ event_set_min_log_level(event, LOG_TYPE_WARNING);
+
++ array_push_back(&request->authdb_event, &event);
++}
++
++void auth_request_passdb_event_end(struct auth_request *request)
++{
++ struct event *event = authdb_event(request);
++ event_unref(&event);
++ array_pop_back(&request->authdb_event);
++
++ /* restore protocol-specific settings */
++ request->set = global_auth_settings;
++}
++
++void auth_request_passdb_lookup_begin(struct auth_request *request)
++{
++ struct event *event;
++
++ i_assert(request->passdb != NULL);
++ i_assert(!request->userdb_lookup);
++ auth_request_passdb_event_begin(request);
++
++ event = authdb_event(request);
++
++ request->passdb_cache_result = AUTH_REQUEST_CACHE_NONE;
++
+ e_debug(event_create_passthrough(event)->
+ set_name("auth_passdb_request_started")->
+ event(),
+ "Performing passdb lookup");
+- array_push_back(&request->authdb_event, &event);
+ }
+
+ void auth_request_passdb_lookup_end(struct auth_request *request,
+@@ -675,8 +694,7 @@ void auth_request_passdb_lookup_end(stru
+ request->set->cache_ttl != 0 && request->set->cache_size != 0)
+ e->add_str("cache", auth_request_cache_result_to_str(request->passdb_cache_result));
+ e_debug(e->event(), "Finished passdb lookup");
+- event_unref(&event);
+- array_pop_back(&request->authdb_event);
++ auth_request_passdb_event_end(request);
+ }
+
+ void auth_request_userdb_lookup_begin(struct auth_request *request)
+--- a/src/auth/auth-request.h
++++ b/src/auth/auth-request.h
+@@ -379,6 +379,9 @@ void auth_request_master_user_login_fini
+ const char *auth_request_get_log_prefix_db(struct auth_request *auth_request);
+ void auth_request_fields_init(struct auth_request *request);
+
++void auth_request_passdb_event_begin(struct auth_request *request);
++void auth_request_passdb_event_end(struct auth_request *request);
++
+ void auth_request_passdb_lookup_begin(struct auth_request *request);
+ void auth_request_passdb_lookup_end(struct auth_request *request,
+ enum passdb_result result);
+
+From 8fd279db61be2054c8f3e7275766717318a2df91 Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi at open-xchange.com>
+Date: Mon, 9 Mar 2026 21:23:29 +0200
+Subject: [PATCH 23/24] auth: Initialize set_credentials event properly
+
+Fixes update_query
+---
+ src/auth/auth-request.c | 4 ++++
+ src/auth/auth-worker-server.c | 2 ++
+ 2 files changed, 6 insertions(+)
+
+--- a/src/auth/auth-request.c
++++ b/src/auth/auth-request.c
+@@ -1345,6 +1345,8 @@ void auth_request_set_credentials(struct
+ struct auth_passdb *passdb = request->passdb;
+ const char *cache_key, *new_credentials;
+
++ auth_request_passdb_event_begin(request);
++
+ cache_key = passdb_cache == NULL ? NULL : passdb->cache_key;
+ if (cache_key != NULL)
+ auth_cache_remove(passdb_cache, request, cache_key);
+@@ -1361,6 +1363,8 @@ void auth_request_set_credentials(struct
+ /* this passdb doesn't support credentials update */
+ callback(FALSE, request);
+ }
++
++ auth_request_passdb_event_end(request);
+ }
+
+ static void auth_request_userdb_save_cache(struct auth_request *request,
+
+From 535a14209810e7b0c8f94479daf580ac4c637904 Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi at open-xchange.com>
+Date: Wed, 11 Mar 2026 12:46:53 +0200
+Subject: [PATCH 24/24] auth: passdb-sql - Require update_query to be set when
+ used
+
+---
+ src/auth/passdb-sql.c | 6 ++++++
+ 1 file changed, 6 insertions(+)
+
+--- a/src/auth/passdb-sql.c
++++ b/src/auth/passdb-sql.c
+@@ -245,6 +245,12 @@ static void sql_set_credentials(struct a
+ return;
+ }
+
++ if (*query == '\0') {
++ e_error(authdb_event(request), "update_query is empty");
++ callback(FALSE, request);
++ return;
++ }
++
+ sql_request = i_new(struct passdb_sql_request, 1);
+ sql_request->auth_request = request;
+ sql_request->callback.set_credentials = callback;
+
diff --git a/CVE-2026-27856.patch b/CVE-2026-27856.patch
new file mode 100644
index 0000000..c3179ca
--- /dev/null
+++ b/CVE-2026-27856.patch
@@ -0,0 +1,120 @@
+From 6ef282fab8af8faec75b0ff8cb87b11094642ba0 Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi at open-xchange.com>
+Date: Wed, 4 Mar 2026 08:05:13 +0200
+Subject: [PATCH 16/24] doveadm: client-connection - Use timing safe credential
+ check
+
+---
+ src/doveadm/client-connection-http.c | 7 +++++--
+ src/doveadm/client-connection-tcp.c | 4 +---
+ 2 files changed, 6 insertions(+), 5 deletions(-)
+
+--- a/src/doveadm/client-connection-http.c
++++ b/src/doveadm/client-connection-http.c
+@@ -984,7 +984,9 @@ doveadm_http_server_auth_basic(struct cl
+ value = p_strdup_printf(conn->conn.pool,
+ "doveadm:%s", set->doveadm_password);
+ base64_encode(value, strlen(value), b64_value);
+- if (creds->data != NULL && strcmp(creds->data, str_c(b64_value)) == 0)
++
++ if (creds->data != NULL &&
++ str_equals_timing_almost_safe(value, creds->data))
+ return TRUE;
+
+ i_error("Invalid authentication attempt to HTTP API "
+@@ -1009,7 +1011,8 @@ doveadm_http_server_auth_api_key(struct
+ b64_value = str_new(conn->conn.pool, 32);
+ base64_encode(set->doveadm_api_key,
+ strlen(set->doveadm_api_key), b64_value);
+- if (creds->data != NULL && strcmp(creds->data, str_c(b64_value)) == 0)
++ if (creds->data != NULL &&
++ str_equals_timing_almost_safe(creds->data, str_c(b64_value)))
+ return TRUE;
+
+ i_error("Invalid authentication attempt to HTTP API "
+--- a/src/doveadm/client-connection-tcp.c
++++ b/src/doveadm/client-connection-tcp.c
+@@ -342,9 +342,7 @@ client_connection_tcp_authenticate(struc
+ return -1;
+ }
+ pass = t_strndup(data + 9, size - 9);
+- if (strlen(pass) != strlen(set->doveadm_password) ||
+- !mem_equals_timing_safe(pass, set->doveadm_password,
+- strlen(pass))) {
++ if (!str_equals_timing_almost_safe(pass, set->doveadm_password)) {
+ i_error("doveadm client authenticated with wrong password");
+ return -1;
+ }
+
+From 6d8c50154744284304ceeab69c3951e6d6852007 Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi at open-xchange.com>
+Date: Wed, 4 Mar 2026 09:28:18 +0200
+Subject: [PATCH 17/24] doveadm: Use datastack for temporary b64 value
+
+There is no need to allocate it from connection pool.
+---
+ src/doveadm/client-connection-http.c | 14 +++++---------
+ 1 file changed, 5 insertions(+), 9 deletions(-)
+
+--- a/src/doveadm/client-connection-http.c
++++ b/src/doveadm/client-connection-http.c
+@@ -972,7 +972,7 @@ doveadm_http_server_auth_basic(struct cl
+ struct client_connection_http *conn = req->conn;
+ const struct doveadm_settings *set = conn->conn.set;
+ string_t *b64_value;
+- char *value;
++ const char *value;
+
+ if (*set->doveadm_password == '\0') {
+ i_error("Invalid authentication attempt to HTTP API: "
+@@ -980,13 +980,11 @@ doveadm_http_server_auth_basic(struct cl
+ return FALSE;
+ }
+
+- b64_value = str_new(conn->conn.pool, 32);
+- value = p_strdup_printf(conn->conn.pool,
+- "doveadm:%s", set->doveadm_password);
+- base64_encode(value, strlen(value), b64_value);
++ value = t_strdup_printf("doveadm:%s", set->doveadm_password);
++ b64_value = t_base64_encode_str(0, UINT_MAX, value);
+
+ if (creds->data != NULL &&
+- str_equals_timing_almost_safe(value, creds->data))
++ str_equals_timing_almost_safe(str_c(b64_value), creds->data))
+ return TRUE;
+
+ i_error("Invalid authentication attempt to HTTP API "
+@@ -1008,9 +1006,7 @@ doveadm_http_server_auth_api_key(struct
+ return FALSE;
+ }
+
+- b64_value = str_new(conn->conn.pool, 32);
+- base64_encode(set->doveadm_api_key,
+- strlen(set->doveadm_api_key), b64_value);
++ b64_value = t_base64_encode_str(0, UINT_MAX, set->doveadm_api_key);
+ if (creds->data != NULL &&
+ str_equals_timing_almost_safe(creds->data, str_c(b64_value)))
+ return TRUE;
+
+From cc628976210de17bd26b4d9761ea879ce78b7980 Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi at open-xchange.com>
+Date: Wed, 4 Mar 2026 14:39:43 +0200
+Subject: [PATCH 18/24] doveadm: client-connection - Get API key from
+ per-connection settings
+
+---
+ src/doveadm/client-connection-http.c | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+--- a/src/doveadm/client-connection-http.c
++++ b/src/doveadm/client-connection-http.c
+@@ -997,7 +997,7 @@ doveadm_http_server_auth_api_key(struct
+ const struct http_auth_credentials *creds)
+ {
+ struct client_connection_http *conn = req->conn;
+- const struct doveadm_settings *set = doveadm_settings;
++ const struct doveadm_settings *set = conn->conn.set;
+ string_t *b64_value;
+
+ if (*set->doveadm_api_key == '\0') {
+
diff --git a/CVE-2026-27857.patch b/CVE-2026-27857.patch
new file mode 100644
index 0000000..fe94a12
--- /dev/null
+++ b/CVE-2026-27857.patch
@@ -0,0 +1,539 @@
+From ad8e6542518acee4e1a803001b232c03732cdaff Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen at open-xchange.com>
+Date: Fri, 6 Mar 2026 15:25:14 +0200
+Subject: [PATCH 12/24] lib-imap, global: Add params parameter to
+ imap_parser_create()
+
+---
+ src/imap-login/imap-login-client.c | 4 ++--
+ src/imap-login/imap-login-cmd-id.c | 4 +++-
+ src/imap/cmd-append.c | 3 ++-
+ src/imap/cmd-setmetadata.c | 3 ++-
+ src/imap/imap-client.c | 3 ++-
+ src/lib-imap-client/imapc-connection.c | 3 ++-
+ src/lib-imap-storage/imap-msgpart.c | 2 +-
+ src/lib-imap/imap-bodystructure.c | 4 ++--
+ src/lib-imap/imap-envelope.c | 2 +-
+ src/lib-imap/imap-parser.c | 3 ++-
+ src/lib-imap/imap-parser.h | 6 +++++-
+ src/lib-imap/test-imap-parser.c | 10 +++++-----
+ src/plugins/virtual/virtual-config.c | 2 +-
+ 13 files changed, 30 insertions(+), 19 deletions(-)
+
+--- a/src/imap-login/imap-login-client.c
++++ b/src/imap-login/imap-login-client.c
+@@ -379,7 +379,7 @@ static void imap_client_create(struct cl
+ imap_client->parser =
+ imap_parser_create(imap_client->common.input,
+ imap_client->common.output,
+- IMAP_LOGIN_MAX_LINE_LENGTH);
++ IMAP_LOGIN_MAX_LINE_LENGTH, NULL);
+ if (imap_client->set->imap_literal_minus)
+ imap_parser_enable_literal_minus(imap_client->parser);
+ client->io = io_add_istream(client->input, client_input, client);
+@@ -416,7 +416,7 @@ static void imap_client_starttls(struct
+ imap_client->parser =
+ imap_parser_create(imap_client->common.input,
+ imap_client->common.output,
+- IMAP_LOGIN_MAX_LINE_LENGTH);
++ IMAP_LOGIN_MAX_LINE_LENGTH, NULL);
+
+ /* CRLF is lost from buffer when streams are reopened. */
+ imap_client->skip_line = FALSE;
+--- a/src/imap-login/imap-login-cmd-id.c
++++ b/src/imap-login/imap-login-cmd-id.c
+@@ -237,7 +237,9 @@ int cmd_id(struct imap_client *client)
+ client->cmd_id = id = i_new(struct imap_client_cmd_id, 1);
+ id->parser = imap_parser_create(client->common.input,
+ client->common.output,
+- IMAP_LOGIN_MAX_LINE_LENGTH);
++ IMAP_LOGIN_MAX_LINE_LENGTH,
++ NULL);
++ id->log_reply = str_new(default_pool, 64);
+ if (client->set->imap_literal_minus)
+ imap_parser_enable_literal_minus(id->parser);
+ parser_flags = IMAP_PARSE_FLAG_STOP_AT_LIST;
+--- a/src/imap/cmd-append.c
++++ b/src/imap/cmd-append.c
+@@ -947,7 +947,8 @@ bool cmd_append(struct client_command_co
+ o_stream_unset_flush_callback(client->output);
+
+ ctx->save_parser = imap_parser_create(client->input, client->output,
+- client->set->imap_max_line_length);
++ client->set->imap_max_line_length,
++ NULL);
+ if (client->set->imap_literal_minus)
+ imap_parser_enable_literal_minus(ctx->save_parser);
+
+--- a/src/imap/cmd-setmetadata.c
++++ b/src/imap/cmd-setmetadata.c
+@@ -289,7 +289,8 @@ cmd_setmetadata_start(struct imap_setmet
+ asynchronously the same way as APPEND does. */
+ client->input_lock = cmd;
+ ctx->parser = imap_parser_create(client->input, client->output,
+- client->set->imap_max_line_length);
++ client->set->imap_max_line_length,
++ NULL);
+ if (client->set->imap_literal_minus)
+ imap_parser_enable_literal_minus(ctx->parser);
+ o_stream_unset_flush_callback(client->output);
+--- a/src/imap/imap-client.c
++++ b/src/imap/imap-client.c
+@@ -958,7 +958,8 @@ client_command_new(struct client *client
+ } else {
+ cmd->parser =
+ imap_parser_create(client->input, client->output,
+- client->set->imap_max_line_length);
++ client->set->imap_max_line_length,
++ NULL);
+ if (client->set->imap_literal_minus)
+ imap_parser_enable_literal_minus(cmd->parser);
+ }
+--- a/src/lib-imap-client/imapc-connection.c
++++ b/src/lib-imap-client/imapc-connection.c
+@@ -1845,7 +1845,8 @@ static void imapc_connection_connect_nex
+ o_stream_set_flush_callback(conn->output, imapc_connection_connected,
+ conn);
+ conn->parser = imap_parser_create(conn->input, NULL,
+- conn->client->set.max_line_length);
++ conn->client->set.max_line_length,
++ NULL);
+ conn->to = timeout_add(conn->client->set.connect_timeout_msecs,
+ imapc_connection_timeout, conn);
+ conn->to_output = timeout_add(conn->client->set.max_idle_time*1000,
+--- a/src/lib-imap-storage/imap-msgpart.c
++++ b/src/lib-imap-storage/imap-msgpart.c
+@@ -148,7 +148,7 @@ imap_msgpart_get_header_fields(pool_t po
+ int result = 0;
+
+ input = i_stream_create_from_data(header_list, strlen(header_list));
+- parser = imap_parser_create(input, NULL, SIZE_MAX);
++ parser = imap_parser_create(input, NULL, SIZE_MAX, NULL);
+
+ if (imap_parser_finish_line(parser, 0, 0, &args) > 0 &&
+ imap_arg_get_list_full(args, &hdr_list, &list_count) &&
+--- a/src/lib-imap/imap-bodystructure.c
++++ b/src/lib-imap/imap-bodystructure.c
+@@ -703,7 +703,7 @@ int imap_bodystructure_parse_full(const
+ input = i_stream_create_from_data(bodystructure, strlen(bodystructure));
+ (void)i_stream_read(input);
+
+- parser = imap_parser_create(input, NULL, SIZE_MAX);
++ parser = imap_parser_create(input, NULL, SIZE_MAX, NULL);
+ ret = imap_parser_finish_line(parser, 0,
+ IMAP_PARSE_FLAG_LITERAL_TYPE, &args);
+ if (ret < 0) {
+@@ -953,7 +953,7 @@ int imap_body_parse_from_bodystructure(c
+ input = i_stream_create_from_data(bodystructure, strlen(bodystructure));
+ (void)i_stream_read(input);
+
+- parser = imap_parser_create(input, NULL, SIZE_MAX);
++ parser = imap_parser_create(input, NULL, SIZE_MAX, NULL);
+ ret = imap_parser_finish_line(parser, 0, IMAP_PARSE_FLAG_NO_UNESCAPE |
+ IMAP_PARSE_FLAG_LITERAL_TYPE, &args);
+ if (ret < 0) {
+--- a/src/lib-imap/imap-envelope.c
++++ b/src/lib-imap/imap-envelope.c
+@@ -222,7 +222,7 @@ bool imap_envelope_parse(const char *env
+ input = i_stream_create_from_data(envelope, strlen(envelope));
+ (void)i_stream_read(input);
+
+- parser = imap_parser_create(input, NULL, SIZE_MAX);
++ parser = imap_parser_create(input, NULL, SIZE_MAX, NULL);
+ ret = imap_parser_finish_line(parser, 0,
+ IMAP_PARSE_FLAG_LITERAL_TYPE, &args);
+ if (ret < 0) {
+--- a/src/lib-imap/imap-parser.c
++++ b/src/lib-imap/imap-parser.c
+@@ -69,7 +69,8 @@ struct imap_parser {
+
+ struct imap_parser *
+ imap_parser_create(struct istream *input, struct ostream *output,
+- size_t max_line_size)
++ size_t max_line_size,
++ const struct imap_parser_params *params ATTR_UNUSED)
+ {
+ struct imap_parser *parser;
+
+--- a/src/lib-imap/imap-parser.h
++++ b/src/lib-imap/imap-parser.h
+@@ -38,6 +38,9 @@ enum imap_parser_error {
+ IMAP_PARSE_ERROR_LITERAL_TOO_BIG
+ };
+
++struct imap_parser_params {
++};
++
+ struct imap_parser;
+
+ /* Create new IMAP argument parser. output is used for sending command
+@@ -53,7 +56,8 @@ struct imap_parser;
+ 2 * max_line_size. */
+ struct imap_parser *
+ imap_parser_create(struct istream *input, struct ostream *output,
+- size_t max_line_size) ATTR_NULL(2);
++ size_t max_line_size,
++ const struct imap_parser_params *params);
+ void imap_parser_ref(struct imap_parser *parser);
+ void imap_parser_unref(struct imap_parser **parser);
+
+--- a/src/lib-imap/test-imap-parser.c
++++ b/src/lib-imap/test-imap-parser.c
+@@ -16,7 +16,7 @@ static void test_imap_parser_crlf(void)
+
+ test_begin("imap parser crlf handling");
+ input = test_istream_create(test_input);
+- parser = imap_parser_create(input, NULL, 1024);
++ parser = imap_parser_create(input, NULL, 1024, NULL);
+
+ /* must return -2 until LF is read */
+ for (i = 0; test_input[i] != '\n'; i++) {
+@@ -60,7 +60,7 @@ static void test_imap_parser_partial_lis
+
+ test_begin("imap parser partial list");
+ input = test_istream_create(test_input);
+- parser = imap_parser_create(input, NULL, 1024);
++ parser = imap_parser_create(input, NULL, 1024, NULL);
+
+ (void)i_stream_read(input);
+ test_assert(imap_parser_read_args(parser, 0,
+@@ -123,7 +123,7 @@ static void test_imap_parser_read_tag_cm
+ if (tests[i].type != COMMAND) {
+ input = test_istream_create(tests[i].input);
+ test_assert(i_stream_read(input) > 0);
+- parser = imap_parser_create(input, NULL, 1024);
++ parser = imap_parser_create(input, NULL, 1024, NULL);
+ ret = imap_parser_read_tag(parser, &atom);
+ test_assert_idx(ret == tests[i].ret, i);
+ test_assert_idx(ret <= 0 || strcmp(tests[i].tag, atom) == 0, i);
+@@ -134,7 +134,7 @@ static void test_imap_parser_read_tag_cm
+ if (tests[i].type != TAG) {
+ input = test_istream_create(tests[i].input);
+ test_assert(i_stream_read(input) > 0);
+- parser = imap_parser_create(input, NULL, 1024);
++ parser = imap_parser_create(input, NULL, 1024, NULL);
+ ret = imap_parser_read_command_name(parser, &atom);
+ test_assert_idx(ret == tests[i].ret, i);
+ test_assert_idx(ret <= 0 || strcmp(tests[i].tag, atom) == 0, i);
+--- a/src/plugins/virtual/virtual-config.c
++++ b/src/plugins/virtual/virtual-config.c
+@@ -54,7 +54,7 @@ virtual_search_args_parse(const string_t
+ input = i_stream_create_from_data(str_data(rule), str_len(rule));
+ (void)i_stream_read(input);
+
+- imap_parser = imap_parser_create(input, NULL, SIZE_MAX);
++ imap_parser = imap_parser_create(input, NULL, SIZE_MAX, NULL);
+ ret = imap_parser_finish_line(imap_parser, 0, 0, &args);
+ if (ret < 0) {
+ sargs = NULL;
+--- a/src/lib-imap/imap-id.c
++++ b/src/lib-imap/imap-id.c
+@@ -111,7 +111,7 @@ const char *imap_id_reply_generate(const
+ input = i_stream_create_from_data(settings, strlen(settings));
+ (void)i_stream_read(input);
+
+- parser = imap_parser_create(input, NULL, SIZE_MAX);
++ parser = imap_parser_create(input, NULL, SIZE_MAX, NULL);
+ if (imap_parser_finish_line(parser, 0, 0, &args) <= 0)
+ ret = "NIL";
+ else
+--- a/src/director/director-test.c
++++ b/src/director/director-test.c
+@@ -250,7 +250,7 @@
+ o_stream_set_no_error_handling(client->output, TRUE);
+ client->io = io_add(fd, IO_READ, imap_client_input, client);
+ client->parser =
+- imap_parser_create(client->input, client->output, 4096);
++ imap_parser_create(client->input, client->output, 4096, NULL);
+ o_stream_nsend_str(client->output,
+ "* OK [CAPABILITY IMAP4rev1] director-test ready.\r\n");
+ DLLIST_PREPEND(&imap_clients, client);
+
+From bb845a61f81be978f641b79058d4bae06f27ee39 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen at open-xchange.com>
+Date: Fri, 6 Mar 2026 15:32:29 +0200
+Subject: [PATCH 13/24] lib-imap: Add imap_parser_params.list_count_limit
+
+---
+ src/lib-imap/imap-parser.c | 15 ++++++++++-
+ src/lib-imap/imap-parser.h | 6 +++++
+ src/lib-imap/test-imap-parser.c | 46 +++++++++++++++++++++++++++++++++
+ 3 files changed, 66 insertions(+), 1 deletion(-)
+
+--- a/src/lib-imap/imap-parser.c
++++ b/src/lib-imap/imap-parser.c
+@@ -39,6 +39,7 @@ struct imap_parser {
+ struct istream *input;
+ struct ostream *output;
+ size_t max_line_size;
++ unsigned int list_count_limit;
+ enum imap_parser_flags flags;
+
+ /* reset by imap_parser_reset(): */
+@@ -46,6 +47,7 @@ struct imap_parser {
+ ARRAY_TYPE(imap_arg_list) root_list;
+ ARRAY_TYPE(imap_arg_list) *cur_list;
+ struct imap_arg *list_arg;
++ unsigned int list_count;
+
+ enum arg_parse_type cur_type;
+ size_t cur_pos; /* parser position in input buffer */
+@@ -70,7 +72,7 @@ struct imap_parser {
+ struct imap_parser *
+ imap_parser_create(struct istream *input, struct ostream *output,
+ size_t max_line_size,
+- const struct imap_parser_params *params ATTR_UNUSED)
++ const struct imap_parser_params *params)
+ {
+ struct imap_parser *parser;
+
+@@ -81,6 +83,10 @@ imap_parser_create(struct istream *input
+ parser->input = input;
+ parser->output = output;
+ parser->max_line_size = max_line_size;
++ if (params != NULL && params->list_count_limit > 0)
++ parser->list_count_limit = params->list_count_limit;
++ else
++ parser->list_count_limit = UINT_MAX;
+
+ p_array_init(&parser->root_list, parser->pool, LIST_INIT_COUNT);
+ parser->cur_list = &parser->root_list;
+@@ -122,6 +128,7 @@ void imap_parser_reset(struct imap_parse
+ p_array_init(&parser->root_list, parser->pool, LIST_INIT_COUNT);
+ parser->cur_list = &parser->root_list;
+ parser->list_arg = NULL;
++ parser->list_count = 0;
+
+ parser->cur_type = ARG_PARSE_NONE;
+ parser->cur_pos = 0;
+@@ -210,6 +217,12 @@ static bool imap_parser_close_list(struc
+ parser->error = IMAP_PARSE_ERROR_BAD_SYNTAX;
+ return FALSE;
+ }
++ if (parser->list_count >= parser->list_count_limit) {
++ parser->error_msg = "Too many '('";
++ parser->error = IMAP_PARSE_ERROR_BAD_SYNTAX;
++ return FALSE;
++ }
++ parser->list_count++;
+
+ arg = imap_arg_create(parser);
+ arg->type = IMAP_ARG_EOL;
+--- a/src/lib-imap/imap-parser.h
++++ b/src/lib-imap/imap-parser.h
+@@ -39,6 +39,12 @@ enum imap_parser_error {
+ };
+
+ struct imap_parser_params {
++ /* How many open lists ('(' chars) to allow before faililng the parsing.
++ 0 means unlimited. This is mainly used to prevent excessive memory
++ usage in imap-login process. In imap process there are many other
++ ways to increase memory usage, so we let the max_line_size be the
++ only limit. */
++ unsigned int list_count_limit;
+ };
+
+ struct imap_parser;
+--- a/src/lib-imap/test-imap-parser.c
++++ b/src/lib-imap/test-imap-parser.c
+@@ -2,6 +2,7 @@
+
+ #include "lib.h"
+ #include "istream.h"
++#include "istream-chain.h"
+ #include "imap-parser.h"
+ #include "test-common.h"
+
+@@ -79,6 +80,50 @@ static void test_imap_parser_partial_lis
+ test_end();
+ }
+
++static void test_imap_parser_list_limit(void)
++{
++ struct {
++ const char *input;
++ int ret;
++ } tests[] = {
++ { "(())\r\n", 1 },
++ { "((()))\r\n", -1 },
++ };
++ struct istream_chain *chain;
++ struct istream *chain_input;
++ struct imap_parser *parser;
++ const struct imap_arg *args;
++
++ test_begin("imap parser list limit");
++ struct imap_parser_params params = {
++ .list_count_limit = 2,
++ };
++
++ for (unsigned int i = 0; i < N_ELEMENTS(tests); i++) {
++ chain_input = i_stream_create_chain(&chain, SIZE_MAX);
++ parser = imap_parser_create(chain_input, NULL, 1024, ¶ms);
++
++ for (unsigned int j = 0; j < 2; j++) {
++ struct istream *input =
++ test_istream_create(tests[i].input);
++ i_stream_chain_append(chain, input);
++ i_stream_unref(&input);
++
++ (void)i_stream_read(chain_input);
++
++ test_assert_cmp(imap_parser_read_args(parser, 0, 0, &args), ==, tests[i].ret);
++ /* skip over CRLF */
++ i_stream_skip(chain_input, i_stream_get_data_size(chain_input));
++
++ /* make sure parser reset works */
++ imap_parser_reset(parser);
++ }
++ imap_parser_unref(&parser);
++ i_stream_destroy(&chain_input);
++ }
++ test_end();
++}
++
+ static void test_imap_parser_read_tag_cmd(void)
+ {
+ enum read_type {
+@@ -150,6 +195,7 @@ int main(void)
+ static void (*const test_functions[])(void) = {
+ test_imap_parser_crlf,
+ test_imap_parser_partial_list,
++ test_imap_parser_list_limit,
+ test_imap_parser_read_tag_cmd,
+ NULL
+ };
+
+From ab084588f615b7f2bb6e18c8ee9176511b647aca Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen at open-xchange.com>
+Date: Fri, 6 Mar 2026 15:35:12 +0200
+Subject: [PATCH 14/24] imap-login: Limit the number of open IMAP parser lists
+
+This prevents attackers from using a large number of '(' in a command to
+grow memory usage excessively.
+---
+ src/imap-login/imap-login-client.c | 10 ++++++++--
+ src/imap-login/imap-login-client.h | 4 ++++
+ src/imap-login/imap-login-cmd-id.c | 6 +++++-
+ 3 files changed, 17 insertions(+), 3 deletions(-)
+
+--- a/src/imap-login/imap-login-client.c
++++ b/src/imap-login/imap-login-client.c
+@@ -375,11 +375,14 @@ static void imap_client_create(struct cl
+ {
+ struct imap_client *imap_client = (struct imap_client *)client;
+
++ struct imap_parser_params params = {
++ .list_count_limit = IMAP_LOGIN_LIST_COUNT_LIMIT,
++ };
+ imap_client->set = other_sets[0];
+ imap_client->parser =
+ imap_parser_create(imap_client->common.input,
+ imap_client->common.output,
+- IMAP_LOGIN_MAX_LINE_LENGTH, NULL);
++ IMAP_LOGIN_MAX_LINE_LENGTH, ¶ms);
+ if (imap_client->set->imap_literal_minus)
+ imap_parser_enable_literal_minus(imap_client->parser);
+ client->io = io_add_istream(client->input, client_input, client);
+@@ -412,11 +415,14 @@ static void imap_client_starttls(struct
+ {
+ struct imap_client *imap_client = (struct imap_client *)client;
+
++ struct imap_parser_params params = {
++ .list_count_limit = IMAP_LOGIN_LIST_COUNT_LIMIT,
++ };
+ imap_parser_unref(&imap_client->parser);
+ imap_client->parser =
+ imap_parser_create(imap_client->common.input,
+ imap_client->common.output,
+- IMAP_LOGIN_MAX_LINE_LENGTH, NULL);
++ IMAP_LOGIN_MAX_LINE_LENGTH, ¶ms);
+
+ /* CRLF is lost from buffer when streams are reopened. */
+ imap_client->skip_line = FALSE;
+--- a/src/imap-login/imap-login-client.h
++++ b/src/imap-login/imap-login-client.h
+@@ -11,6 +11,10 @@
+ /* maximum length for IMAP command line. */
+ #define IMAP_LOGIN_MAX_LINE_LENGTH 8192
+
++/* Maximum number of '(' allowed in an IMAP command. Pre-login only uses
++ lists in the ID command. */
++#define IMAP_LOGIN_LIST_COUNT_LIMIT 1
++
+ enum imap_client_id_state {
+ IMAP_CLIENT_ID_STATE_LIST = 0,
+ IMAP_CLIENT_ID_STATE_KEY,
+--- a/src/imap-login/imap-login-cmd-id.c
++++ b/src/imap-login/imap-login-cmd-id.c
+@@ -235,10 +235,14 @@ int cmd_id(struct imap_client *client)
+
+ if (client->cmd_id == NULL) {
+ client->cmd_id = id = i_new(struct imap_client_cmd_id, 1);
++
++ struct imap_parser_params params = {
++ .list_count_limit = IMAP_LOGIN_LIST_COUNT_LIMIT,
++ };
+ id->parser = imap_parser_create(client->common.input,
+ client->common.output,
+ IMAP_LOGIN_MAX_LINE_LENGTH,
+- NULL);
++ ¶ms);
+ id->log_reply = str_new(default_pool, 64);
+ if (client->set->imap_literal_minus)
+ imap_parser_enable_literal_minus(id->parser);
+
+From e0b953af76d754c2984c2867b71fccfef5231bf5 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen at open-xchange.com>
+Date: Mon, 2 Mar 2026 13:50:24 +0200
+Subject: [PATCH 15/24] global: Use const for struct imap_parser_params params
+
+---
+ src/imap-login/imap-login-client.c | 4 ++--
+ src/imap-login/imap-login-cmd-id.c | 2 +-
+ src/lib-imap/test-imap-parser.c | 2 +-
+ 3 files changed, 4 insertions(+), 4 deletions(-)
+
+--- a/src/imap-login/imap-login-client.c
++++ b/src/imap-login/imap-login-client.c
+@@ -375,7 +375,7 @@ static void imap_client_create(struct cl
+ {
+ struct imap_client *imap_client = (struct imap_client *)client;
+
+- struct imap_parser_params params = {
++ const struct imap_parser_params params = {
+ .list_count_limit = IMAP_LOGIN_LIST_COUNT_LIMIT,
+ };
+ imap_client->set = other_sets[0];
+@@ -415,7 +415,7 @@ static void imap_client_starttls(struct
+ {
+ struct imap_client *imap_client = (struct imap_client *)client;
+
+- struct imap_parser_params params = {
++ const struct imap_parser_params params = {
+ .list_count_limit = IMAP_LOGIN_LIST_COUNT_LIMIT,
+ };
+ imap_parser_unref(&imap_client->parser);
+--- a/src/imap-login/imap-login-cmd-id.c
++++ b/src/imap-login/imap-login-cmd-id.c
+@@ -236,7 +236,7 @@ int cmd_id(struct imap_client *client)
+ if (client->cmd_id == NULL) {
+ client->cmd_id = id = i_new(struct imap_client_cmd_id, 1);
+
+- struct imap_parser_params params = {
++ const struct imap_parser_params params = {
+ .list_count_limit = IMAP_LOGIN_LIST_COUNT_LIMIT,
+ };
+ id->parser = imap_parser_create(client->common.input,
+--- a/src/lib-imap/test-imap-parser.c
++++ b/src/lib-imap/test-imap-parser.c
+@@ -95,7 +95,7 @@ static void test_imap_parser_list_limit(
+ const struct imap_arg *args;
+
+ test_begin("imap parser list limit");
+- struct imap_parser_params params = {
++ const struct imap_parser_params params = {
+ .list_count_limit = 2,
+ };
+
+
diff --git a/CVE-2026-27859.patch b/CVE-2026-27859.patch
new file mode 100644
index 0000000..a5f7547
--- /dev/null
+++ b/CVE-2026-27859.patch
@@ -0,0 +1,98 @@
+From 4041a0cb7bbaa6faf38670ab92afc1039dd0d0d5 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen at open-xchange.com>
+Date: Tue, 24 Feb 2026 13:11:14 +0200
+Subject: [PATCH 03/24] lib-mail: Limit the number of RFC2231 parameters that
+ can be parsed
+
+This avoids excessive CPU usage especially in result_append().
+---
+ src/lib-mail/rfc2231-parser.c | 4 +++-
+ src/lib-mail/rfc822-parser.h | 5 +++++
+ src/lib-mail/test-rfc2231-parser.c | 30 ++++++++++++++++++++++++++++++
+ 3 files changed, 38 insertions(+), 1 deletion(-)
+
+--- a/src/lib-mail/rfc2231-parser.c
++++ b/src/lib-mail/rfc2231-parser.c
+@@ -45,7 +45,7 @@ int rfc2231_parse(struct rfc822_parser_c
+ const struct rfc2231_parameter *rfc2231_params;
+ const char *key, *p, *p2;
+ string_t *str;
+- unsigned int i, j, count, next, next_idx;
++ unsigned int i, j, count, next, next_idx, params_count = 0;
+ bool ok, have_extended, broken = FALSE;
+ const char *prev_replacement_str;
+ int ret;
+@@ -63,6 +63,8 @@ int rfc2231_parse(struct rfc822_parser_c
+ t_array_init(&rfc2231_params_arr, 8);
+ str = t_str_new(64);
+ while ((ret = rfc822_parse_content_param(ctx, &key, str)) != 0) {
++ if (++params_count > RFC2231_MAX_PARAMS)
++ break;
+ if (ret < 0) {
+ /* try to continue anyway.. */
+ broken = TRUE;
+--- a/src/lib-mail/rfc822-parser.h
++++ b/src/lib-mail/rfc822-parser.h
+@@ -3,6 +3,11 @@
+
+ #include "unichar.h"
+
++/* Maximum number of parameters to parse. After this the rest of the parameters
++ are skipped. This is to avoid excessive CPU usage that can be caused by
++ merging of these parameters. */
++#define RFC2231_MAX_PARAMS 128
++
+ /* This can be used as a common NUL replacement character */
+ #define RFC822_NUL_REPLACEMENT_STR UNICODE_REPLACEMENT_CHAR_UTF8
+
+--- a/src/lib-mail/test-rfc2231-parser.c
++++ b/src/lib-mail/test-rfc2231-parser.c
+@@ -1,6 +1,7 @@
+ /* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+ #include "lib.h"
++#include "str.h"
+ #include "rfc822-parser.h"
+ #include "rfc2231-parser.h"
+ #include "test-common.h"
+@@ -41,10 +42,39 @@ static void test_rfc2231_parser(void)
+ test_end();
+ }
+
++static void test_rfc2231_parser_limits(void)
++{
++ string_t *input = t_str_new(1024);
++
++ test_begin("rfc2231 parser limits");
++ str_append(input, "; ");
++ for (unsigned int i = 0; i < 1100; i++)
++ str_printfa(input, "a%u=b%u; ", i, i);
++ struct rfc822_parser_context parser;
++ const char *const *result;
++ rfc822_parser_init(&parser, str_data(input), str_len(input), NULL);
++ test_assert(rfc2231_parse(&parser, &result) == 0);
++
++ unsigned int count = str_array_length(result);
++ test_assert(count == RFC2231_MAX_PARAMS * 2);
++ for (unsigned int i = 0; i < count; i += 2) {
++ str_truncate(input, 0);
++ str_printfa(input, "a%u", i / 2);
++ test_assert_strcmp_idx(result[i], str_c(input), i);
++
++ str_truncate(input, 0);
++ str_printfa(input, "b%u", i / 2);
++ test_assert_strcmp_idx(result[i + 1], str_c(input), i);
++ }
++ rfc822_parser_deinit(&parser);
++ test_end();
++}
++
+ int main(void)
+ {
+ static void (*const test_functions[])(void) = {
+ test_rfc2231_parser,
++ test_rfc2231_parser_limits,
+ NULL
+ };
+ return test_run(test_functions);
+
diff --git a/CVE-2026-33603.patch b/CVE-2026-33603.patch
new file mode 100644
index 0000000..57c3f64
--- /dev/null
+++ b/CVE-2026-33603.patch
@@ -0,0 +1,37 @@
+Backport of:
+
+From c1c53885bda550632b944dd305013cd010e0e058 Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi at open-xchange.com>
+Date: Wed, 8 Apr 2026 11:33:11 +0300
+Subject: [PATCH] login-common: Only accept base64 in sasl
+
+---
+ src/login-common/client-common-auth.c | 9 +++++++++
+ 1 file changed, 9 insertions(+)
+
+--- a/src/login-common/client-common-auth.c
++++ b/src/login-common/client-common-auth.c
+@@ -3,6 +3,7 @@
+ #include "hostpid.h"
+ #include "login-common.h"
+ #include "array.h"
++#include "base64.h"
+ #include "iostream.h"
+ #include "istream.h"
+ #include "ostream.h"
+@@ -655,6 +656,14 @@ client_auth_handle_reply(struct client *
+
+ void client_auth_respond(struct client *client, const char *response)
+ {
++ /* Only accept base64 */
++ for (size_t i = 0; response[i] != '\0'; i++) {
++ if (!base64_is_valid_char(response[i]) && response[i] != '=') {
++ client_auth_fail(client, "Invalid base64 in response");
++ return;
++ }
++ }
++
+ client->auth_waiting = FALSE;
+ client_set_auth_waiting(client);
+ auth_client_request_continue(client->auth_request, response);
+
diff --git a/CVE-2026-40020.patch b/CVE-2026-40020.patch
new file mode 100644
index 0000000..7891a44
--- /dev/null
+++ b/CVE-2026-40020.patch
@@ -0,0 +1,129 @@
+From b7daa4104ff064c1fb549540cc9d96c2d9e2509c Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen at open-xchange.com>
+Date: Wed, 22 Apr 2026 15:43:58 +0300
+Subject: [PATCH 1/3] acl: Add acl_id_is_valid()
+
+Returns TRUE if the ACL identifier string is at most ACL_ID_MAX_LEN
+(1024) bytes long, contains no control characters and is valid UTF-8.
+---
+ src/plugins/acl/acl-rights.c | 16 ++++++++++++++++
+ src/plugins/acl/acl-rights.h | 6 ++++++
+ 2 files changed, 22 insertions(+)
+
+Index: 2.3/src/plugins/acl/acl-api.c
+===================================================================
+--- a/src/plugins/acl/acl-api.c
++++ b/src/plugins/acl/acl-api.c
+@@ -3,6 +3,7 @@
+ #include "lib.h"
+ #include "array.h"
+ #include "str.h"
++#include "unichar.h"
+ #include "strescape.h"
+ #include "hash.h"
+ #include "mail-user.h"
+@@ -11,6 +12,8 @@
+ #include "acl-cache.h"
+ #include "acl-api-private.h"
+
++#include <ctype.h>
++
+ struct acl_letter_map {
+ char letter;
+ const char *name;
+@@ -269,6 +272,19 @@ int acl_backend_nonowner_lookups_rebuild
+ return backend->v.nonowner_lookups_rebuild(backend);
+ }
+
++bool acl_id_is_valid(const char *id)
++{
++ size_t len = strlen(id);
++
++ if (len > ACL_ID_MAX_LEN)
++ return FALSE;
++ for (size_t i = 0; i < len; i++) {
++ if (i_iscntrl(id[i]))
++ return FALSE;
++ }
++ return uni_utf8_data_is_valid((const unsigned char *)id, len);
++}
++
+ void acl_rights_write_id(string_t *dest, const struct acl_rights *right)
+ {
+ switch (right->id_type) {
+Index: 2.3/src/plugins/acl/acl-api.h
+===================================================================
+--- a/src/plugins/acl/acl-api.h
++++ b/src/plugins/acl/acl-api.h
+@@ -35,6 +35,8 @@ struct acl_object;
+ #define MAILBOX_ATTRIBUTE_PREFIX_ACL \
+ MAILBOX_ATTRIBUTE_PREFIX_DOVECOT_PVT"acl/"
+
++#define ACL_ID_MAX_LEN 1024
++
+ /* ACL identifiers in override order */
+ enum acl_id_type {
+ /* Anyone's rights, including anonymous's.
+@@ -164,4 +166,8 @@ int acl_object_list_deinit(struct acl_ob
+ /* Returns the canonical ID for the right. */
+ const char *acl_rights_get_id(const struct acl_rights *right);
+
++/* Returns TRUE if the ACL identifier string is valid: no longer than
++ ACL_ID_MAX_LEN bytes, no control characters and valid UTF-8. */
++bool acl_id_is_valid(const char *id);
++
+ #endif
+
+From 20b48c3db5fed7ccaa8e0a4c10ca54f6dc36a63d Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen at open-xchange.com>
+Date: Wed, 22 Apr 2026 15:44:24 +0300
+Subject: [PATCH 2/3] imap-acl: Fail if ACL identifier is invalid
+
+Reject invalid identifiers early in imap_acl_identifier_parse() using
+acl_id_is_valid(). This prevents CR/LF injection and rejects identifiers
+that are too long, contain control characters or are not valid UTF-8.
+---
+ src/plugins/imap-acl/imap-acl-plugin.c | 5 +++++
+ 1 file changed, 5 insertions(+)
+
+Index: 2.3/src/plugins/imap-acl/imap-acl-plugin.c
+===================================================================
+--- a/src/plugins/imap-acl/imap-acl-plugin.c
++++ b/src/plugins/imap-acl/imap-acl-plugin.c
+@@ -802,6 +802,11 @@ imap_acl_identifier_parse(struct client_
+ {
+ struct mail_user *user = cmd->client->user;
+
++ if (!acl_id_is_valid(id)) {
++ *client_error_r = "Invalid identifier";
++ return -1;
++ }
++
+ if (str_begins(id, IMAP_ACL_GLOBAL_PREFIX)) {
+ *client_error_r = t_strdup_printf("Global ACLs can't be modified: %s",
+ id);
+
+From 1cf6ad1a119e5dace816e401e73ba6cc11d1472e Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen at open-xchange.com>
+Date: Wed, 22 Apr 2026 15:45:00 +0300
+Subject: [PATCH 3/3] acl: Assert-crash if ACL identifier is invalid before
+ writing it
+
+It should have been checked earlier already.
+---
+ src/plugins/acl/acl-backend-vfile-update.c | 1 +
+ 1 file changed, 1 insertion(+)
+
+Index: 2.3/src/plugins/acl/acl-backend-vfile-update.c
+===================================================================
+--- a/src/plugins/acl/acl-backend-vfile-update.c
++++ b/src/plugins/acl/acl-backend-vfile-update.c
+@@ -116,6 +116,7 @@ vfile_write_right(string_t *dest, const
+ if (neg) str_append_c(dest,'-');
+ acl_rights_write_id(dest, right);
+
++ i_assert(acl_id_is_valid(str_c(dest)));
+ if (strchr(str_c(dest), ' ') != NULL) T_BEGIN {
+ /* need to escape it */
+ const char *escaped = t_strdup(str_escape(str_c(dest)));
+
diff --git a/CVE-2026-42006.patch b/CVE-2026-42006.patch
new file mode 100644
index 0000000..3e462ab
--- /dev/null
+++ b/CVE-2026-42006.patch
@@ -0,0 +1,99 @@
+From da1438c76b797f055d4ad7f0eaa17e5e29ca31ee Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen at open-xchange.com>
+Date: Mon, 27 Apr 2026 17:40:46 +0300
+Subject: [PATCH] lib-imap: Fix imap_parser_params.list_count_limit to actually
+ work
+
+The previous fix in d0f67b52914565a35f3817335ab9633cb291513c was
+accidentally limiting the number of ')', not the number of '('.
+---
+ src/lib-imap/imap-parser.c | 19 +++++++++++--------
+ src/lib-imap/test-imap-parser.c | 14 ++++++++++++--
+ 2 files changed, 23 insertions(+), 10 deletions(-)
+
+diff --git a/src/lib-imap/imap-parser.c b/src/lib-imap/imap-parser.c
+index 6212aed33d8..b1df3d7b788 100644
+--- a/src/lib-imap/imap-parser.c
++++ b/src/lib-imap/imap-parser.c
+@@ -191,8 +191,15 @@ static struct imap_arg *imap_arg_create(struct imap_parser *parser)
+ return arg;
+ }
+
+-static void imap_parser_open_list(struct imap_parser *parser)
++static bool imap_parser_open_list(struct imap_parser *parser)
+ {
++ if (parser->list_count >= parser->list_count_limit) {
++ parser->error_msg = "Too many '('";
++ parser->error = IMAP_PARSE_ERROR_BAD_SYNTAX;
++ return FALSE;
++ }
++ parser->list_count++;
++
+ parser->list_arg = imap_arg_create(parser);
+ parser->list_arg->type = IMAP_ARG_LIST;
+ p_array_init(&parser->list_arg->_data.list, parser->pool,
+@@ -200,6 +207,7 @@ static void imap_parser_open_list(struct imap_parser *parser)
+ parser->cur_list = &parser->list_arg->_data.list;
+
+ parser->cur_type = ARG_PARSE_NONE;
++ return TRUE;
+ }
+
+ static bool imap_parser_close_list(struct imap_parser *parser)
+@@ -217,12 +225,6 @@ static bool imap_parser_close_list(struct imap_parser *parser)
+ parser->error = IMAP_PARSE_ERROR_BAD_SYNTAX;
+ return FALSE;
+ }
+- if (parser->list_count >= parser->list_count_limit) {
+- parser->error_msg = "Too many '('";
+- parser->error = IMAP_PARSE_ERROR_BAD_SYNTAX;
+- return FALSE;
+- }
+- parser->list_count++;
+
+ arg = imap_arg_create(parser);
+ arg->type = IMAP_ARG_EOL;
+@@ -673,7 +675,8 @@ static bool imap_parser_read_arg(struct imap_parser *parser)
+ parser->literal8 = FALSE;
+ break;
+ case '(':
+- imap_parser_open_list(parser);
++ if (!imap_parser_open_list(parser))
++ return FALSE;
+ if ((parser->flags & IMAP_PARSE_FLAG_STOP_AT_LIST) != 0) {
+ i_stream_skip(parser->input, 1);
+ return FALSE;
+diff --git a/src/lib-imap/test-imap-parser.c b/src/lib-imap/test-imap-parser.c
+index fb7c308a233..d509c8176de 100644
+--- a/src/lib-imap/test-imap-parser.c
++++ b/src/lib-imap/test-imap-parser.c
+@@ -85,9 +85,15 @@ static void test_imap_parser_list_limit(void)
+ struct {
+ const char *input;
+ int ret;
++ const char *error;
+ } tests[] = {
+- { "(())\r\n", 1 },
+- { "((()))\r\n", -1 },
++ { "(())\r\n", 1, NULL },
++ { "((\r\n", -1, "Missing ')'" },
++ { "(()\r\n", -1, "Missing ')'" },
++ { "(()))\r\n", -1, "Unexpected ')'" },
++ { "((()))\r\n", -1, "Too many '('" },
++ { "(({10}\r\n", -2, NULL },
++ { "((({10}\r\n", -1, "Too many '('" },
+ };
+ struct istream_chain *chain;
+ struct istream *chain_input;
+@@ -112,6 +118,10 @@ static void test_imap_parser_list_limit(void)
+ (void)i_stream_read(chain_input);
+
+ test_assert_cmp(imap_parser_read_args(parser, 0, 0, &args), ==, tests[i].ret);
++ if (tests[i].ret == -1) {
++ enum imap_parser_error err;
++ test_assert_strcmp_idx(imap_parser_get_error(parser, &err), tests[i].error, i);
++ }
+ /* skip over CRLF */
+ i_stream_skip(chain_input, i_stream_get_data_size(chain_input));
+
+
================================================================
---- gitweb:
http://git.pld-linux.org/gitweb.cgi/packages/dovecot.git/commitdiff/6d79fd7a09598640f4b0c6562dc0654517555c0a
More information about the pld-cvs-commit
mailing list