commit bfe32b5c6ea033736a26da8421513206db9fe305
Author: Heiko Schlittermann (HS12-RIPE) <hs@schlittermann.de>
Date:   Tue Feb 18 19:13:40 2025 +0100

    Squashed from fix/cve-2025-26794 (fixes CVE-26794)
    
    commits:
    - 7f6698462878112cdf0492a20ebe2c741add6e13
    - cb555353adf0b3a2636cc871c798d6a3dc883ccf
    - 9c3a6612aa38f959f5066fe4fa3ffeb8d2ac41d9
    - ce7afaaa984a6abf7f96aab7c2deacd7313ab4fa
    - 2e5a01d44a311f83e80428c51c3e176516c8dfee
    - dd47d83b492c3de1f3fa897932458b000d3b89d6
    - d2535688188d6df4cb99ff8d7ef4d6bfed0d8b00
    - a893031446b24fb3270a7a7435f4eb599586406c
    - fef415a3ec1c58d85bb6feec5833c534ca9d600b
    - 621d158fec79ef0df6d1dcebfbd50515f616424a
    - 48181b3ab70924ae6b1ad6c4c8c10f10d8994046
    - e073dfcfb5ccb807d5d19a130dc40e1343f6f665
    - ce244262d5f8bee25231608d75ff3b500d6e3b81
    - bf36dce31f105f1798625cd2508b05d56dc501b4
    - 2487665952d853725fedef21d3fc10d6130008d2
    - 95c259a14ec9c5a888e0a90e9cf16b80bde45ec1
    - 1b59cf192ae9a0b3d56a33ae60c6e5c5b96548f3
    - 1f11530a3b06fc07963974932ff9a58e18e008f6
    - 1c70fe1448e79605bcd59ee86b2b8608d71cda89
    - 385b54d67c2100c2614ade31b9c9f2176c99f58a
    - 0b3c72c5b809efe1098e5df7b4de3f251f785dc3

diff --git a/src/src/hintsdb.h b/src/src/hintsdb.h
index a6555ed25..6d71c994c 100644
--- a/src/src/hintsdb.h
+++ b/src/src/hintsdb.h
@@ -66,6 +66,15 @@ required by Exim's process transisitions)?
 #ifndef HINTSDB_H
 #define HINTSDB_H
 
+# if COMPILE_UTILITY
+# undef DEBUG
+# define DEBUG(...) if (getenv("DEBUG"))
+# define debug_printf_indent(x, y...) fprintf(stderr, "# " x, y)
+# define debug_printf(x, y...) fprintf(stderr, "# " x, y)
+# else
+extern void debug_printf_indent(const char *, ...);
+# endif
+static inline BOOL is_tainted(const void *);
 
 #ifdef USE_SQLITE
 # if defined(USE_DB) || defined(USE_GDBM) || defined(USE_TDB)
@@ -89,6 +98,27 @@ required by Exim's process transisitions)?
 
 # /* Access functions */
 
+/* The key must be zero terminated, an empty key has len == 1. */
+static inline BOOL
+is_cstring(EXIM_DATUM *key)
+{
+if (key->len < 1)
+  {
+# ifdef SQL_DEBUG
+  fprintf(stderr, "invalid key length %d (must be >= 1)\n", key->len);
+# endif
+  return FALSE;
+  }
+if (key->data[key->len-1] != '\0')
+  {
+# ifdef SQL_DEBUG
+  fprintf(stderr, "key %.*s is not zero terminated\n", key->len, key->data);
+# endif
+  return FALSE;
+  }
+return TRUE;
+}
+
 static inline BOOL
 exim_lockfile_needed(void)
 {
@@ -119,68 +149,75 @@ if ((ret = sqlite3_open_v2(CCS name, &dbp, sflags, NULL)) == SQLITE_OK)
 return ret == SQLITE_OK ? dbp : NULL;
 }
 
-/* EXIM_DBGET - returns TRUE if successful, FALSE otherwise */
-/* note we alloc'n'copy - the caller need not do so */
-/* result has a NUL appended, but the length is as per the DB */
-
 static inline BOOL
-exim_dbget__(EXIM_DB * dbp, const uschar * s, EXIM_DATUM * res)
+exim_dbget__(EXIM_DB * dbp, EXIM_DATUM * key, EXIM_DATUM * res)
 {
-sqlite3_stmt * statement;
-int ret;
+int ret = FALSE;
+sqlite3_stmt * stmt = NULL; /* don't make it static, as it depends on the dbp */
+const char query[] = "SELECT dat FROM tbl WHERE ky = ?";
 
-res->len = (size_t) -1;
-/* fprintf(stderr, "exim_dbget__(%s)\n", s); */
-if ((ret = sqlite3_prepare_v2(dbp, CCS s, -1, &statement, NULL)) != SQLITE_OK)
+if (SQLITE_OK != sqlite3_prepare_v2(dbp, query, sizeof(query)-1, &stmt, NULL))
   {
-/* fprintf(stderr, "prepare fail: %s\n", sqlite3_errmsg(dbp)); */
-  return FALSE;
+# ifdef SQL_DEBUG
+  fprintf(stderr, EXIM_DBTYPE " prepare %s: %s\n", query, sqlite3_errmsg(dbp));
+# endif
+  goto DONE;
   }
-if (sqlite3_step(statement) != SQLITE_ROW)
+
+# ifdef SQL_DEBUG
+DEBUG(D_hints_lookup) debug_printf_indent("prepared SQL: %s\n", sqlite3_sql(stmt));
+# endif
+
+if (SQLITE_OK != sqlite3_bind_text(stmt, 1, CCS key->data, key->len-1, SQLITE_STATIC))
   {
-/* fprintf(stderr, "step fail: %s\n", sqlite3_errmsg(dbp)); */
-  sqlite3_finalize(statement);
-  return FALSE;
+# ifdef SQL_DEBUG
+  fprintf(stderr, EXIM_DBTYPE " bind text (%s): %s\n", sqlite3_sql(stmt), sqlite3_errmsg(dbp));
+# endif
+  goto DONE;
   }
 
-res->len = sqlite3_column_bytes(statement, 0);
+# ifdef SQL_DEBUG
+DEBUG(D_hints_lookup) debug_printf_indent("expanded SQL: %s\n", sqlite3_expanded_sql(stmt));
+# endif
+
+if (SQLITE_ROW != sqlite3_step(stmt))
+  {
+# ifdef SQL_DEBUG
+  DEBUG(D_hints_lookup) debug_printf_indent("step (%s): %s\n", sqlite3_expanded_sql(stmt), sqlite3_errmsg(dbp));
+# endif
+  goto DONE;
+  }
+
+res->len = sqlite3_column_bytes(stmt, 0);
+
 # ifdef COMPILE_UTILITY
-if (!(res->data = malloc(res->len +1)))
-  { sqlite3_finalize(statement); return FALSE; }
+if (!(res->data = malloc(res->len +1))) goto DONE;
 # else
 res->data = store_get(res->len +1, GET_TAINTED);
 # endif
-memcpy(res->data, sqlite3_column_blob(statement, 0), res->len);
+
+memcpy(res->data, sqlite3_column_blob(stmt, 0), res->len);
 res->data[res->len] = '\0';
 /* fprintf(stderr, "res %d bytes: '%.*s'\n", (int)res->len, (int)res->len, res->data); */
-sqlite3_finalize(statement);
-return TRUE;
+
+ret = TRUE;
+
+DONE:
+sqlite3_finalize(stmt);
+
+return ret;
 }
 
+/* EXIM_DBGET - returns the value associated with the key. The key must
+be zero terminated, an empty key has len == 1. */
 static inline BOOL
 exim_dbget(EXIM_DB * dbp, EXIM_DATUM * key, EXIM_DATUM * res)
 {
-# define FMT "SELECT dat FROM tbl WHERE ky = '%.*s';"
-uschar * qry;
-int i;
-BOOL ret;
-
-# ifdef COMPILE_UTILITY
-/* fprintf(stderr, "exim_dbget(k len %d '%.*s')\n", (int)key->len, (int)key->len, key->data); */
-i = snprintf(NULL, 0, FMT, (int) key->len, key->data)+1;
-if (!(qry = malloc(i)))
-  return FALSE;
-snprintf(CS qry, i, FMT, (int) key->len, key->data);
-ret = exim_dbget__(dbp, qry, res);
-free(qry);
-# else
-/* fprintf(stderr, "exim_dbget(k len %d '%.*s')\n", (int)key->len, (int)key->len, key->data); */
-qry = string_sprintf(FMT, (int) key->len, key->data);
-ret = exim_dbget__(dbp, qry, res);
+# ifdef SQL_DEBUG
+DEBUG(D_hints_lookup) debug_printf_indent(EXIM_DBTYPE " get key: len=%d, strlen=%d, key=%.*s\n", key->len, Ustrlen(key->data), key->len, key->data);
 # endif
-
-return ret;
-# undef FMT
+if (!is_cstring(key)) return FALSE;
+return exim_dbget__(dbp, key, res);
 }
 
 /**/
@@ -190,46 +227,83 @@ return ret;
 static inline int
 exim_s_dbp(EXIM_DB * dbp, EXIM_DATUM * key, EXIM_DATUM * data, const uschar * alt)
 {
-int hlen = data->len * 2, off = 0, res;
-# define FMT "INSERT OR %s INTO tbl (ky,dat) VALUES ('%.*s', X'%.*s');"
-uschar * qry;
+const char sql[] = "INSERT OR %s INTO tbl (ky, dat) VALUES(?, ?)";
+int ret = EXIM_DBPUTB_DUP;
+sqlite3_stmt *stmt = NULL;
+uschar * query;
+
 # ifdef COMPILE_UTILITY
-uschar * hex = malloc(hlen+1);
-if (!hex) return EXIM_DBPUTB_DUP;	/* best we can do */
+int i = 1 + snprintf(NULL, 0, sql, alt);
+if (NULL == (query = US malloc(i)))
+  {
+  fprintf(stderr, "can't allocate memory for %s", sql);
+  return EXIM_DBPUTB_DUP;
+  }
+snprintf(CS query, i, sql, alt);
 # else
-uschar * hex = store_get(hlen+1, data->data);
+query = string_sprintf(sql, alt);
 # endif
 
-for (const uschar * s = data->data, * t = s + data->len; s < t; s++, off += 2)
-  sprintf(CS hex + off, "%02X", *s);
+if (SQLITE_OK != sqlite3_prepare_v2(dbp, CCS query, -1, &stmt, NULL))
+  {
+# ifdef SQL_DEBUG
+  fprintf(stderr, EXIM_DBTYPE " prepare %s: %s\n", query, sqlite3_errmsg(dbp));
+# endif
+  goto DONE;
+  }
 
-# ifdef COMPILE_UTILITY
-res = snprintf(CS hex, 0, FMT, alt, (int) key->len, key->data, hlen, hex) +1;
-if (!(qry = malloc(res))) return EXIM_DBPUTB_DUP;
-snprintf(CS qry, res, FMT, alt, (int) key->len, key->data, hlen, hex);
-/* fprintf(stderr, "exim_s_dbp(%s)\n", qry); */
-res = sqlite3_exec(dbp, CS qry, NULL, NULL, NULL);
-free(qry);
-free(hex);
-# else
-qry = string_sprintf(FMT, alt, (int) key->len, key->data, hlen, hex);
-/* fprintf(stderr, "exim_s_dbp(%s)\n", qry); */
-res = sqlite3_exec(dbp, CS qry, NULL, NULL, NULL);
-/* fprintf(stderr, "exim_s_dbp res %d\n", res); */
+# ifdef SQL_DEBUG
+DEBUG(D_hints_lookup) debug_printf_indent("prepared SQL: %s\n", sqlite3_sql(stmt));
 # endif
 
-if (res != SQLITE_OK)
-  fprintf(stderr, "sqlite3_exec: %s\n", sqlite3_errmsg(dbp));
+if (SQLITE_OK != sqlite3_bind_text(stmt, 1, CCS key->data, key->len-1, NULL))
+  {
+# ifdef SQL_DEBUG
+  fprintf(stderr, EXIM_DBTYPE " bind to value 1: %s\n", sqlite3_errmsg(dbp));
+# endif
+  goto DONE;
+  }
 
-return res == SQLITE_OK ? EXIM_DBPUTB_OK : EXIM_DBPUTB_DUP;
-# undef FMT
-}
+if (SQLITE_OK != sqlite3_bind_blob(stmt, 2, data->data, data->len, NULL))
+  {
+# ifdef SQL_DEBUG
+  fprintf(stderr, EXIM_DBTYPE " bind to value 2: %s\n", sqlite3_errmsg(dbp));
+# endif
+  goto DONE;
+  }
 
-/* EXIM_DBPUT - returns nothing useful, assumes replace mode */
+# ifdef SQL_DEBUG
+DEBUG(D_hints_lookup) debug_printf_indent("expanded SQL: %s\n", sqlite3_expanded_sql(stmt));
+# endif
+
+if (SQLITE_DONE != sqlite3_step(stmt))
+  {
+# ifdef SQL_DEBUG
+  fprintf(stderr, EXIM_DBTYPE " step (%s): %s\n", sqlite3_expanded_sql(stmt), sqlite3_errmsg(dbp));
+# endif
+  goto DONE;
+  }
+
+ret = EXIM_DBPUTB_OK;
 
+DONE:
+sqlite3_finalize(stmt);
+# ifdef COMPILE_UTILITY
+free(query);
+# endif
+
+return ret;
+}
+
+/* EXIM_DBPUT - returns nothing useful, assumes replace mode
+The key must be zero terminated. An empty key has len == 1. */
 static inline int
 exim_dbput(EXIM_DB * dbp, EXIM_DATUM * key, EXIM_DATUM * data)
 {
+# ifdef SQL_DEBUG
+DEBUG(D_hints_lookup) debug_printf_indent(EXIM_DBTYPE " put: key: len=%d, strlen=%d, key=%.*s\n", key->len, Ustrlen(key->data), key->len, key->data);
+# endif
+if (!is_cstring(key)) return -1;
 /* fprintf(stderr, "exim_dbput()\n"); */
 (void) exim_s_dbp(dbp, key, data, US"REPLACE");
 return 0;
@@ -249,26 +323,52 @@ return exim_s_dbp(dbp, key, data, US"ABORT");
 static inline int
 exim_dbdel(EXIM_DB * dbp, EXIM_DATUM * key)
 {
-# define FMT "DELETE FROM tbl WHERE ky = '%.*s';"
-uschar * qry;
-int res;
+int res = -1;
+sqlite3_stmt *stmt = NULL; /* don't make it static, because it depends on the dbp */
+const char query[] = "DELETE FROM tbl WHERE ky = ?";
 
-# ifdef COMPILE_UTILITY
-res = snprintf(NULL, 0, FMT, (int) key->len, key->data) +1; /* res includes nul */
-if (!(qry = malloc(res))) return SQLITE_NOMEM;
-snprintf(CS qry, res, FMT, (int) key->len, key->data);
-res = sqlite3_exec(dbp, CS qry, NULL, NULL, NULL);
-free(qry);
-# else
-qry = string_sprintf(FMT, (int) key->len, key->data);
-res = sqlite3_exec(dbp, CS qry, NULL, NULL, NULL);
+DEBUG(D_hints_lookup) debug_printf_indent(EXIM_DBTYPE " del key: len=%d, strlen=%d, key=%.*s\n", key->len, Ustrlen(key->data), key->len, key->data);
+if (!is_cstring(key)) return -1;
+
+if (SQLITE_OK != sqlite3_prepare_v2(dbp, query, sizeof(query)-1, &stmt, NULL))
+  {
+# ifdef SQL_DEBUG
+  fprintf(stderr, EXIM_DBTYPE " prepare %s: %s\n", query, sqlite3_errmsg(dbp));
 # endif
+  goto DONE;
+  }
+
+# ifdef SQL_DEBUG
+DEBUG(D_hints_lookup) debug_printf_indent("query: %s\n", sqlite3_sql(stmt));
+# endif
+
+if (SQLITE_OK != sqlite3_bind_text(stmt, 1, CCS key->data, key->len-1, SQLITE_STATIC))
+  {
+# ifdef SQL_DEBUG
+  fprintf(stderr, EXIM_DBTYPE " bind value 1: %s\n", sqlite3_errmsg(dbp));
+# endif
+  goto DONE;
+  }
+
+# ifdef SQL_DEBUG
+DEBUG(D_hints_lookup) debug_printf_indent("expanded query: %s\n", sqlite3_expanded_sql(stmt));
+# endif
+
+if (SQLITE_DONE != sqlite3_step(stmt))
+  {
+# ifdef SQL_DEBUG
+  fprintf(stderr, EXIM_DBTYPE " step: %s: %s\n", sqlite3_expanded_sql(stmt), sqlite3_errmsg(dbp));
+# endif
+  goto DONE;
+  }
 
+res = 0;
+
+DONE:
+sqlite3_finalize(stmt);
 return res;
-# undef FMT
 }
 
-
 /* EXIM_DBCREATE_CURSOR - initialize for scanning operation */
 /* Cursors are inefficiently emulated by repeating searches */
 
@@ -286,33 +386,66 @@ return c;
 }
 
 /* EXIM_DBSCAN */
-/* Note that we return the (next) key, not the record value */
+/* Note that we return the (next) key, not the record value.
+ * We've to add the zero terminator, as this isn't stored in the database */
 static inline BOOL
-exim_dbscan(EXIM_DB * dbp, EXIM_DATUM * key, EXIM_DATUM * res, BOOL first,
-  EXIM_CURSOR * cursor)
+exim_dbscan(EXIM_DB * dbp, EXIM_DATUM * key, EXIM_DATUM * res /* unusied */, BOOL first /*unused*/, EXIM_CURSOR * cursor)
 {
-# define FMT "SELECT ky FROM tbl ORDER BY ky LIMIT 1 OFFSET %d;"
-uschar * qry;
-int i;
-BOOL ret;
+BOOL more = FALSE;
+sqlite3_stmt *stmt = NULL;
+const char query[] = "SELECT ky FROM tbl ORDER BY ky LIMIT 1 OFFSET ?";
 
-# ifdef COMPILE_UTILITY
-i = snprintf(NULL, 0, FMT, *cursor)+1;
-if (!(qry = malloc(i))) return FALSE;
-snprintf(CS qry, i, FMT, *cursor);
-/* fprintf(stderr, "exim_dbscan(%s)\n", qry); */
-ret = exim_dbget__(dbp, qry, key);
-free(qry);
-/* fprintf(stderr, "exim_dbscan ret %c\n", ret ? 'T':'F'); */
-# else
-qry = string_sprintf(FMT, *cursor);
-/* fprintf(stderr, "exim_dbscan(%s)\n", qry); */
-ret = exim_dbget__(dbp, qry, key);
-/* fprintf(stderr, "exim_dbscan ret %c\n", ret ? 'T':'F'); */
+if (SQLITE_OK != sqlite3_prepare_v2(dbp, query, sizeof(query)-1, &stmt, NULL))
+  {
+# ifdef SQL_DEBUG
+  fprintf(stderr, EXIM_DBTYPE " prepare %s: %s\n", query, sqlite3_errmsg(dbp));
 # endif
-if (ret) *cursor = *cursor + 1;
-return ret;
-# undef FMT
+  goto DONE;
+  }
+
+# ifdef SQL_DEBUG
+DEBUG(D_hints_lookup) debug_printf_indent("prepared query: %s\n", sqlite3_sql(stmt));
+# endif
+
+if (SQLITE_OK != sqlite3_bind_int(stmt, 1, *cursor))
+  {
+# ifdef SQL_DEBUG
+  fprintf(stderr, EXIM_DBTYPE " bind value 1: %s\n", query, sqlite3_errmsg(dbp));
+# endif
+  goto DONE;
+  }
+
+# ifdef SQL_DEBUG
+DEBUG(D_hints_lookup) debug_printf_indent("expanded query: %s\n", sqlite3_expanded_sql(stmt));
+# endif
+
+switch (sqlite3_step(stmt))
+  {
+    case SQLITE_DONE: goto DONE;
+    case SQLITE_ROW: (*cursor)++;
+                      key->len = sqlite3_column_bytes(stmt, 0);
+#ifdef COMPILE_UTILITY
+                      if (!(key->data = malloc(key->len+1))) goto DONE;
+#else
+                      key->data = store_get(key->len+1, GET_TAINTED); // TAINTED? We're talking about the key!
+#endif
+                      memcpy(key->data, sqlite3_column_blob(stmt, 0), key->len);
+                      key->data[key->len] = '\0';
+# ifdef SQL_DEBUG
+                      DEBUG(D_hints_lookup) debug_printf_indent("key length=%d, val=%s\n", key->len, key->data);
+# endif
+                      more = TRUE;
+                      goto DONE;
+    default:
+# ifdef SQL_DEBUG
+                      fprintf(stderr, EXIM_DBTYPE " step: %s: %s\n", sqlite3_expanded_sql(stmt), sqlite3_errmsg(dbp));
+# endif
+                      goto DONE;
+  }
+
+DONE:
+sqlite3_finalize(stmt);
+return more;
 }
 
 /* EXIM_DBDELETE_CURSOR - terminate scanning operation. */
@@ -326,7 +459,6 @@ store_free(cursor);
 # endif
 }
 
-
 /* EXIM_DBCLOSE */
 static void
 exim_dbclose__(EXIM_DB * dbp)
@@ -335,16 +467,16 @@ exim_dbclose__(EXIM_DB * dbp)
 sqlite3_close(dbp);
 }
 
-
 /* Datum access */
 
 static uschar *
 exim_datum_data_get(EXIM_DATUM * dp)
 { return US dp->data; }
+
 static void
 exim_datum_data_set(EXIM_DATUM * dp, void * s)
 { dp->data = s; }
- 
+
 static unsigned
 exim_datum_size_get(EXIM_DATUM * dp)
 { return dp->len; }
@@ -352,8 +484,6 @@ static void
 exim_datum_size_set(EXIM_DATUM * dp, unsigned n)
 { dp->len = n; }
 
-
-
 static inline void
 exim_datum_init(EXIM_DATUM * dp)
 { dp->data = NULL; }			/* compiler quietening */
@@ -368,11 +498,6 @@ exim_datum_free(EXIM_DATUM * dp)
 
 # define EXIM_DB_RLIMIT	150
 
-
-
-
-
-
 #elif defined(USE_TDB)
 
 # if defined(USE_DB) || defined(USE_GDBM) || defined(USE_SQLITE)
@@ -513,11 +638,6 @@ d->dptr = NULL;
 
 # define EXIM_DB_RLIMIT	150
 
-
-
-
-
-
 /********************* Berkeley db native definitions **********************/
 
 #elif defined USE_DB
@@ -574,13 +694,11 @@ at DB release 4.3. */
 static inline void
 dbfn_bdb_error_callback(const DB_ENV * dbenv, const char * pfx, const char * msg)
 {
-#ifndef MACRO_PREDEF 
+#ifndef MACRO_PREDEF
 log_write(0, LOG_MAIN, "Berkeley DB error: %s", msg);
 #endif
 }
 
-
-
 /* Access functions (BDB 4.1+) */
 
 static inline BOOL
@@ -841,7 +959,6 @@ exim_datum_free(EXIM_DATUM * d)
 
 #  endif
 
-
 #  else /* DB_VERSION_MAJOR >= 3 */
 #   error Berkeley DB versions earlier than 3 are not supported */
 #  endif /* DB_VERSION_MAJOR */
@@ -849,17 +966,11 @@ exim_datum_free(EXIM_DATUM * d)
 #  error Berkeley DB version 1 is no longer supported
 # endif /* DB_VERSION_STRING */
 
-
 /* all BDB versions */
 /* size limit */
 
 # define EXIM_DB_RLIMIT	150
 
-
-
-
-
-
 /********************* gdbm interface definitions **********************/
 
 #elif defined USE_GDBM
@@ -1007,15 +1118,9 @@ exim_datum_free(EXIM_DATUM * d)
 
 #else  /* USE_GDBM */
 
-
-
-
-
-
 /* If none of USE_DB, USG_GDBM, USE_SQLITE or USE_TDB are set,
 the default is the NDBM interface (which seems to be a wrapper for GDBM) */
 
-
 /********************* ndbm interface definitions **********************/
 
 # include <ndbm.h>
@@ -1145,10 +1250,6 @@ exim_datum_free(EXIM_DATUM * d)
 
 #endif /* !USE_GDBM */
 
-
-
-
-
 #if defined(COMPILE_UTILITY) || defined(MACRO_PREDEF)
 
 static inline EXIM_DB *
@@ -1166,9 +1267,6 @@ exim_dbclose(EXIM_DB * dbp)
 
 /* Wrappers for open/close with debug tracing */
 
-extern void debug_printf_indent(const char *, ...);
-static inline BOOL is_tainted(const void *);
-
 static inline EXIM_DB *
 exim_dbopen(const uschar * name, const uschar * dirname, int flags,
   unsigned mode)
@@ -1204,7 +1302,11 @@ exim_dbclose__(dbp);
 
 /********************* End of dbm library definitions **********************/
 
-
+# if COMPILE_UTILITY
+# undef debug_printf_indent
+# undef debug_printf
+# undef DEBUG
+# endif
 #endif	/* whole file */
 /* End of hintsdb.h */
 /* vi: aw ai sw=2
diff --git a/src/src/smtp_in.c b/src/src/smtp_in.c
index c52d3f4d6..b80e075e6 100644
--- a/src/src/smtp_in.c
+++ b/src/src/smtp_in.c
@@ -5576,14 +5576,27 @@ while (done <= 0)
 	break;
 	}
 
-      /* Compute the serialization key for this command. */
+      /* Compute the serialization key for this command. We used (all the way
+      back to 4.00) to include the given string as part of the key, but this
+      opens a security hole for hintsdb types that use a command-string for
+      operations. All ETRN with the same command hash are serialized */
 
-      etrn_serialize_key = string_sprintf("etrn-%s\n", smtp_cmd_data);
+      md5 hash;
+      uschar *digest = store_get(16, GET_TAINTED);
+
+      md5_start(&hash);
+      md5_end(&hash, smtp_cmd_argument, Ustrlen(smtp_cmd_argument), digest);
+
+      etrn_serialize_key = string_sprintf("etrn-" /* don't we have a function doing exactly this? */
+          "%02x%02x%02x%02x" "%02x%02x%02x%02x"   /* we have, since 2024-09-xx we can use %.16H */
+          "%02x%02x%02x%02x" "%02x%02x%02x%02x",
+          digest[0], digest[1], digest[2], digest[3], digest[4], digest[5], digest[6], digest[7],
+          digest[8], digest[9], digest[10], digest[11], digest[12], digest[13], digest[14], digest[15]);
 
       /* If a command has been specified for running as a result of ETRN, we
-      permit any argument to ETRN. If not, only the # standard form is permitted,
-      since that is strictly the only kind of ETRN that can be implemented
-      according to the RFC. */
+      permit any argument to ETRN. If not, only the # standard form is
+      permitted, since that is strictly the only kind of ETRN that can be
+      implemented according to the RFC. */
 
       GET_OPTION("smtp_etrn_command");
       if (smtp_etrn_command)
@@ -5592,8 +5605,8 @@ while (done <= 0)
 	BOOL rc;
 	etrn_command = smtp_etrn_command;
 	deliver_domain = smtp_cmd_data;
-	rc = transport_set_up_command(&argv, smtp_etrn_command, TSUC_EXPAND_ARGS, 0, NULL,
-	  US"ETRN processing", &error);
+	rc = transport_set_up_command(&argv, smtp_etrn_command,
+			TSUC_EXPAND_ARGS, 0, NULL, US"ETRN processing", &error);
 	deliver_domain = NULL;
 	if (!rc)
 	  {
