[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, &params);
++
++		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, &params);
+ 	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, &params);
+ 
+ 	/* 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);
++						&params);
+ 		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