[Git][debian-proftpd-team/proftpd][upstream] New upstream version 1.3.9b~dfsg

Hilmar Preuße (@hilmar) gitlab at salsa.debian.org
Sun Jun 7 22:21:40 BST 2026



Hilmar Preuße pushed to branch upstream at Debian ProFTPD Team / proftpd


Commits:
6d6fe6ac by Hilmar Preuße at 2026-06-07T23:07:19+02:00
New upstream version 1.3.9b~dfsg
- - - - -


25 changed files:

- .github/workflows/ci.yml
- .github/workflows/legacy-platforms-ci.yml
- NEWS
- README.md
- RELEASE_NOTES
- contrib/dist/rpm/proftpd.spec
- contrib/mod_digest.c
- contrib/mod_quotatab.c
- contrib/mod_sftp/Makefile.in
- contrib/mod_sftp/crypto.c
- contrib/mod_sftp/crypto.h
- contrib/mod_sftp/fxp.c
- contrib/mod_sftp/kex.c
- contrib/mod_sftp/keys.c
- contrib/mod_sftp/mac.c
- contrib/mod_sftp/mod_sftp.c
- contrib/mod_sftp/packet.c
- + contrib/mod_sftp/provider.c
- + contrib/mod_sftp/provider.h
- contrib/mod_sql.c
- contrib/mod_tls.c
- contrib/mod_wrap2_sql.c
- include/version.h
- + tests/t/lib/ProFTPD/Tests/Modules/mod_sftp/quotatab.pm
- + tests/t/modules/mod_sftp/quotatab.t


Changes:

=====================================
.github/workflows/ci.yml
=====================================
@@ -44,7 +44,7 @@ jobs:
       #  https://github.com/actions/checkout/issues/1809.
       ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true
 
-      PACKAGE_VERSION: 1.3.9a
+      PACKAGE_VERSION: 1.3.9b
       REDIS_HOST: redis
 
     strategy:


=====================================
.github/workflows/legacy-platforms-ci.yml
=====================================
@@ -44,7 +44,7 @@ jobs:
       #  https://github.com/actions/checkout/issues/1809.
       ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true
 
-      PACKAGE_VERSION: 1.3.9a
+      PACKAGE_VERSION: 1.3.9b
       REDIS_HOST: redis
 
     strategy:


=====================================
NEWS
=====================================
@@ -15,6 +15,26 @@
   where `N' is the issue number.
 -----------------------------------------------------------------------------
 
+1.3.9b - Released 02-Jun-2026
+--------------------------------
+- Issue 2057 - SQL Injection in mod_wrap2_sql via reverse DNS
+  hostname (CVE-2026-44331).
+- Issue 2056 - Incomplete fix for session management with OpenSSL 3.2.x or
+  later, when using TLSv1.2 or earlier.  This complements the fix for
+  Issue #1963.
+- Issue 2098 - Hard quota limits on uploads do not cause SFTP WRITE requests
+  to fail as expected.
+- Issue 2102 - SSH payload length underflow calculation for ETM/ChaChaPoly
+  algorithms in mod_sftp.
+- Issue 2104 - SSH packet with empty payload triggers null pointer dereference
+  in mod_sftp.
+- Issue 2106 - Bad DSA signatures can lead to out-of-bounds read of heap memory
+  in mod_sftp.
+- Issue 2108 - Mismatched RSA/DSA algorithm signatures can lead to null
+  dereference in mod_sftp.
+- Issue 2115 - SFTP request payload length underflow calculation in mod_sftp.
+- Issue 2120 - Several modules fail to build using OpenSSL 4.0.
+
 1.3.9a - Released 27-Apr-2026
 --------------------------------
 - Issue 1886 - SCP transfers fail for files with spaces in their names.


=====================================
README.md
=====================================
@@ -7,7 +7,7 @@
 [![Coverage Status](https://coveralls.io/repos/github/proftpd/proftpd/badge.svg?branch=master)](https://coveralls.io/github/proftpd/proftpd?branch=master)
 [![CodeQL Analysis](https://github.com/proftpd/proftpd/actions/workflows/codeql.yml/badge.svg)](https://github.com/proftpd/proftpd/actions/workflows/codeql.yml)
 [![Coverity Scan Status](https://scan.coverity.com/projects/198/badge.svg)](https://scan.coverity.com/projects/198)
-[![Release](https://img.shields.io/badge/release-1.3.9a-brightgreen)](https://github.com/proftpd/proftpd/releases/latest)
+[![Release](https://img.shields.io/badge/release-1.3.9b-brightgreen)](https://github.com/proftpd/proftpd/releases/latest)
 [![License](https://img.shields.io/badge/license-GPL-brightgreen.svg)](https://img.shields.io/badge/license-GPL-brightgreen.svg)
 
 ## Introduction


=====================================
RELEASE_NOTES
=====================================
@@ -6,6 +6,15 @@ This file contains a description of the major changes to ProFTPD for the
 releases.  More information on these changes can be found in the NEWS and
 ChangeLog files.
 
+1.3.9b
+---------
+
+  + Additional fixes for SQL injection (CVE-2026-42167), notably for handling
+    `%{env:...}` and `%{note:...}` variables.
+
+  + Updated modules to build against OpenSSL 4.x.
+
+
 1.3.9a
 ---------
 


=====================================
contrib/dist/rpm/proftpd.spec
=====================================
@@ -53,7 +53,7 @@
 # RHEL5 and clones don't have suitably recent versions of pcre/libmemcached
 # so use --with rhel5 to inhibit those features when using --with everything
 
-%global proftpd_version			1.3.9a
+%global proftpd_version			1.3.9b
 
 # rc_version should be incremented for each RC release, and reset back to 1
 # AFTER each stable release.
@@ -61,7 +61,7 @@
 
 # release_version should be incremented for each maint release, and reset back
 # to 1 BEFORE starting new release cycle.
-%global release_version			2
+%global release_version			3
 
 %if %(echo %{proftpd_version} | grep rc >/dev/null 2>&1 && echo 1 || echo 0)
 %global rpm_version %(echo %{proftpd_version} | sed -e 's/rc.*//')


=====================================
contrib/mod_digest.c
=====================================
@@ -82,6 +82,14 @@
 # define HAVE_LIBRESSL  1
 #endif
 
+#if OPENSSL_VERSION_NUMBER >= 0x40000000L && !defined(HAVE_LIBRESSL)
+# include <openssl/core.h>
+# include <openssl/core_dispatch.h>
+# include <openssl/core_names.h>
+# include <openssl/params.h>
+# include <openssl/provider.h>
+#endif /* OpenSSL 4.x and later */
+
 module digest_module;
 
 static int digest_caching = TRUE;
@@ -169,6 +177,7 @@ static unsigned long digest_algos = DIGEST_DEFAULT_ALGOS;
 
 static const EVP_MD *digest_hash_md = NULL;
 static unsigned long digest_hash_algo = DIGEST_ALGO_SHA1;
+static int digest_hash_free_md = FALSE;
 
 /* Flags for determining the style of hash function names. */
 #define DIGEST_ALGO_FL_IANA_STYLE	0x0001
@@ -180,6 +189,10 @@ static unsigned long digest_hash_algo = DIGEST_ALGO_SHA1;
 # define DIGEST_PROGRESS_NTH_ITER	40000
 #endif
 
+#if OPENSSL_VERSION_NUMBER >= 0x40000000L && !defined(HAVE_LIBRESSL)
+static OSSL_PROVIDER *crc32_provider = NULL;
+#endif /* OpenSSL 4.x and later */
+
 static const char *trace_channel = "digest";
 
 /* Necessary prototypes. */
@@ -229,7 +242,7 @@ static char *digest_bin2hex(pool *p, const unsigned char *buf, size_len,
 
   return hex;
 }
-#endif
+#endif /* ProFTPD 1.3.6 and earlier */
 
 /* CRC32 implementation, as OpenSSL EVP_MD.  The following OpenSSL files
  * used as templates:
@@ -318,6 +331,131 @@ static int CRC32_Free(CRC32_CTX *ctx) {
   return 1;
 }
 
+#if OPENSSL_VERSION_NUMBER >= 0x40000000L && !defined(HAVE_LIBRESSL)
+
+/* Our custom CRC32 digest provider implementation. */
+
+static void *crc32_ctx_new(void *vctx) {
+  CRC32_CTX *ctx;
+
+  ctx = OPENSSL_zalloc(sizeof(CRC32_CTX));
+  return ctx;
+}
+
+static void crc32_ctx_free(void *vctx) {
+  CRC32_CTX *ctx;
+
+  ctx = vctx;
+  CRC32_Free(ctx);
+  OPENSSL_free(ctx);
+}
+
+static int crc32_md_init(void *vctx) {
+  CRC32_CTX *ctx;
+
+  ctx = vctx;
+  return CRC32_Init(ctx);
+}
+
+static int crc32_md_update(void *vctx, const unsigned char *data, size_t len) {
+  CRC32_CTX *ctx;
+
+  ctx = vctx;
+  return CRC32_Update(ctx, data, len);
+}
+
+static int crc32_md_final(void *vctx, unsigned char *out, size_t *out_len,
+    size_t outsz) {
+  CRC32_CTX *ctx;
+
+  ctx = vctx;
+
+  *out_len = outsz;
+
+  if (outsz != 0) {
+    return CRC32_Final(out, ctx);
+  }
+
+  return 1;
+}
+
+static const OSSL_PARAM crc32_params[] = {
+  OSSL_PARAM_size_t(OSSL_DIGEST_PARAM_BLOCK_SIZE, NULL),
+  OSSL_PARAM_size_t(OSSL_DIGEST_PARAM_SIZE, NULL),
+  OSSL_PARAM_END
+};
+
+static int crc32_get_params(void *provctx, OSSL_PARAM params[]) {
+  OSSL_PARAM *p;
+  int ok = 1;
+
+  p = OSSL_PARAM_locate(params, OSSL_DIGEST_PARAM_BLOCK_SIZE);
+  if (p != NULL) {
+    if (OSSL_PARAM_set_size_t(p, CRC32_BLOCK) != 1) {
+      ok = 0;
+    }
+  }
+
+  if (ok == 1) {
+    p = OSSL_PARAM_locate(params, OSSL_DIGEST_PARAM_SIZE);
+    if (p != NULL) {
+      if (OSSL_PARAM_set_size_t(p, CRC32_DIGEST_LENGTH) != 1) {
+        ok = 0;
+      }
+    }
+  }
+
+  return ok;
+}
+
+static const OSSL_PARAM *crc32_gettable_params(void) {
+  return crc32_params;
+}
+
+static const OSSL_DISPATCH crc32_md_functions[] = {
+  { OSSL_FUNC_DIGEST_NEWCTX, (void (*)(void)) crc32_ctx_new },
+  { OSSL_FUNC_DIGEST_FREECTX, (void (*)(void)) crc32_ctx_free },
+  { OSSL_FUNC_DIGEST_INIT, (void (*)(void)) crc32_md_init },
+  { OSSL_FUNC_DIGEST_UPDATE, (void (*)(void)) crc32_md_update },
+  { OSSL_FUNC_DIGEST_FINAL, (void (*)(void)) crc32_md_final },
+  { OSSL_FUNC_DIGEST_GET_PARAMS, (void (*)(void)) crc32_get_params },
+  { OSSL_FUNC_DIGEST_GETTABLE_PARAMS, (void (*)(void)) crc32_gettable_params },
+
+  { 0, NULL }
+};
+
+static const OSSL_ALGORITHM crc32_digests[] = {
+  { "crc32", NULL, crc32_md_functions },
+
+  { NULL, NULL, NULL }
+};
+
+static const OSSL_ALGORITHM *crc32_provider_operation(void *provctx,
+    int operation_id, int *no_cache) {
+  *no_cache = 0;
+
+  if (operation_id == OSSL_OP_DIGEST) {
+    return crc32_digests;
+  }
+
+  return NULL;
+}
+
+static const OSSL_DISPATCH crc32_provider_functions[] = {
+  { OSSL_FUNC_PROVIDER_QUERY_OPERATION, (void (*)(void)) crc32_provider_operation },
+
+  { 0, NULL }
+};
+
+static int crc32_provider_init(const OSSL_CORE_HANDLE *core,
+    const OSSL_DISPATCH *in, const OSSL_DISPATCH **out, void **provctx) {
+  *out = crc32_provider_functions;
+  *provctx = (void *) core;
+
+  return 1;
+}
+
+#else
 static int crc32_init(EVP_MD_CTX *ctx) {
   void *md_data;
 
@@ -411,6 +549,7 @@ static const EVP_MD *EVP_crc32(void) {
 
   return md;
 }
+#endif /* OpenSSL before 4.x */
 
 static const char *get_errors(void) {
   unsigned int count = 0;
@@ -611,28 +750,28 @@ MODRET set_digestalgorithms(cmd_rec *cmd) {
         algos |= DIGEST_ALGO_CRC32;
 
       } else if (strcasecmp(cmd->argv[i], "md5") == 0) {
-#ifndef OPENSSL_NO_MD5
+#if !defined(OPENSSL_NO_MD5)
         algos |= DIGEST_ALGO_MD5;
 #else
         CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "installed OpenSSL does not support the '", cmd->argv[i], "' DigestAlgorithm", NULL));
 #endif /* OPENSSL_NO_MD5 */
 
       } else if (strcasecmp(cmd->argv[i], "sha1") == 0) {
-#ifndef OPENSSL_NO_SHA
+#if !defined(OPENSSL_NO_SHA)
         algos |= DIGEST_ALGO_SHA1;
 #else
         CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "installed OpenSSL does not support the '", cmd->argv[i], "' DigestAlgorithm", NULL));
 #endif /* OPENSSL_NO_SHA */
 
       } else if (strcasecmp(cmd->argv[i], "sha256") == 0) {
-#ifndef OPENSSL_NO_SHA256
+#if !defined(OPENSSL_NO_SHA256)
         algos |= DIGEST_ALGO_SHA256;
 #else
         CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "installed OpenSSL does not support the '", cmd->argv[i], "' DigestAlgorithm", NULL));
 #endif /* OPENSSL_NO_SHA256 */
 
       } else if (strcasecmp(cmd->argv[i], "sha512") == 0) {
-#ifndef OPENSSL_NO_SHA512
+#if !defined(OPENSSL_NO_SHA512)
         algos |= DIGEST_ALGO_SHA512;
 #else
         CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "installed OpenSSL does not support the '", cmd->argv[i], "' DigestAlgorithm", NULL));
@@ -1026,11 +1165,11 @@ static int can_digest_file(pool *p, const char *path, off_t start, size_t len,
 static int blacklisted_file(const char *path) {
   int res = FALSE;
 
-  if (strncasecmp("/dev/full", path, 10) == 0 ||
-      strncasecmp("/dev/null", path, 10) == 0 ||
-      strncasecmp("/dev/random", path, 12) == 0 ||
-      strncasecmp("/dev/urandom", path, 13) == 0 ||
-      strncasecmp("/dev/zero", path, 10) == 0) {
+  if (strcasecmp("/dev/full", path) == 0 ||
+      strcasecmp("/dev/null", path) == 0 ||
+      strcasecmp("/dev/random", path) == 0 ||
+      strcasecmp("/dev/urandom", path) == 0 ||
+      strcasecmp("/dev/zero", path) == 0) {
     res = TRUE;
   }
 
@@ -1044,7 +1183,7 @@ static int compute_digest(pool *p, const char *path, off_t start, off_t len,
   pr_fh_t *fh;
   struct stat st;
   unsigned char *buf;
-  size_t bufsz, readsz, iter_count;
+  size_t bufsz, readsz;
 #if OPENSSL_VERSION_NUMBER < 0x10100000L || \
     (defined(HAVE_LIBRESSL) && LIBRESSL_VERSION_NUMBER < 0x3050000L)
   EVP_MD_CTX ctx;
@@ -1130,13 +1269,10 @@ static int compute_digest(pool *p, const char *path, off_t start, off_t len,
     readsz = len;
   }
 
-  iter_count = 0;
   res = pr_fsio_read(fh, (char *) buf, readsz);
   xerrno = errno;
 
   while (len > 0) {
-    iter_count++;
-
     if (res < 0 &&
         errno == EAGAIN) {
       /* Add a small delay by treating this as EINTR. */
@@ -1201,12 +1337,35 @@ static int compute_digest(pool *p, const char *path, off_t start, off_t len,
   return 0;
 }
 
-static const EVP_MD *get_algo_md(unsigned long algo) {
+static void free_algo_md(const EVP_MD *md) {
+#if (OPENSSL_VERSION_NUMBER >= 0x30000000L && !defined(HAVE_LIBRESSL)) || \
+     (defined(HAVE_LIBRESSL) && LIBRESSL_VERSION_NUMBER >= 0x3080000L)
+  EVP_MD_free((EVP_MD *) md);
+#else
+  /* Avoid compiler warnings. */
+  (void) md;
+#endif /* OpenSSL-3.x/LibreSSL-3.8.x and later */
+}
+
+static const EVP_MD *get_algo_md(unsigned long algo, int *free_md) {
   const EVP_MD *md = NULL;
 
+  *free_md = FALSE;
+
   switch (algo) {
     case DIGEST_ALGO_CRC32:
+#if OPENSSL_VERSION_NUMBER >= 0x40000000L && !defined(HAVE_LIBRESSL)
+      md = EVP_MD_fetch(NULL, "crc32", NULL);
+      if (md == NULL) {
+        pr_trace_msg(trace_channel, 3, "error obtaining CRC32 EVP_MD: %s",
+          get_errors());
+
+      } else {
+        *free_md = TRUE;
+      }
+#else
       md = EVP_crc32();
+#endif /* OpenSSL before 4.x */
       break;
 
 #if !defined(OPENSSL_NO_MD5)
@@ -1537,14 +1696,18 @@ static int add_cached_digest(pool *p, cmd_rec *cmd, unsigned long algo,
   algo_name = get_algo_name(algo, 0);
   if (pr_table_add(cmd->notes, "mod_digest.algo",
       pstrdup(cmd->pool, algo_name), 0) <  0) {
-    pr_trace_msg(trace_channel, 3,
+    if (errno != EEXIST) {
+      pr_trace_msg(trace_channel, 3,
       "error adding 'mod_digest.algo' note: %s", strerror(errno));
+    }
   }
 
   if (pr_table_add(cmd->notes, "mod_digest.digest",
       pstrdup(cmd->pool, hex_digest), 0) < 0) {
-    pr_trace_msg(trace_channel, 3,
-      "error adding 'mod_digest.digest' note: %s", strerror(errno));
+    if (errno != EEXIST) {
+      pr_trace_msg(trace_channel, 3,
+        "error adding 'mod_digest.digest' note: %s", strerror(errno));
+    }
   }
 
   res = pr_table_add(cache, cache_key->key, (void *) cache_key->hex_digest, 0);
@@ -1682,7 +1845,7 @@ static int check_cache_size(cmd_rec *cmd) {
   if (cache_size >= digest_cache_max_size) {
     int xerrno = EAGAIN;
 
-#ifdef EBUSY
+#if defined(EBUSY)
     /* This errno value may not be available on all platforms, but it is
      * the most appropriate.
      */
@@ -1708,7 +1871,7 @@ static int check_cache_size(cmd_rec *cmd) {
 
 static char *get_digest(cmd_rec *cmd, unsigned long algo, const char *path,
     time_t mtime, off_t start, size_t len, int flags) {
-  int res;
+  int res, free_md = FALSE;
   const EVP_MD *md;
   unsigned char *digest = NULL;
   unsigned int digest_len;
@@ -1751,13 +1914,17 @@ static char *get_digest(cmd_rec *cmd, unsigned long algo, const char *path,
     return hex_digest;
   }
 
-  md = get_algo_md(algo);
+  md = get_algo_md(algo, &free_md);
   digest_len = EVP_MD_size(md);
   digest = palloc(cmd->tmp_pool, digest_len);
 
   res = compute_digest(cmd->tmp_pool, path, start, len, md, digest,
     &digest_len, &mtime);
   if (res < 0) {
+    if (free_md == TRUE) {
+      free_algo_md(md);
+    }
+
     return NULL;
   }
 
@@ -1793,6 +1960,10 @@ static char *get_digest(cmd_rec *cmd, unsigned long algo, const char *path,
     }
   }
 
+  if (free_md == TRUE) {
+    free_algo_md(md);
+  }
+
   return hex_digest;
 }
 
@@ -1883,11 +2054,13 @@ static modret_t *digest_xcmd(cmd_rec *cmd, unsigned long algo) {
 
   } else {
     off_t len, start_pos, end_pos;
+    const EVP_MD *md = NULL;
+    int free_md = FALSE;
 
     if (cmd->argc > 3) {
       char *ptr = NULL;
 
-#ifdef HAVE_STRTOULL
+#if defined(HAVE_STRTOULL)
       start_pos = strtoull(cmd->argv[2], &ptr, 10);
 #else
       start_pos = strtoul(cmd->argv[2], &ptr, 10);
@@ -1901,7 +2074,7 @@ static modret_t *digest_xcmd(cmd_rec *cmd, unsigned long algo) {
       }
 
       ptr = NULL;
-#ifdef HAVE_STRTOULL
+#if defined(HAVE_STRTOULL)
       end_pos = strtoull(cmd->argv[3], &ptr, 10);
 #else
       end_pos = strtoul(cmd->argv[3], &ptr, 10);
@@ -1940,9 +2113,14 @@ static modret_t *digest_xcmd(cmd_rec *cmd, unsigned long algo) {
       return PR_ERROR(cmd);
     }
 
-    if (get_algo_md(algo) != NULL) {
+    md = get_algo_md(algo, &free_md);
+    if (md != NULL) {
       char *hex_digest;
 
+      if (free_md == TRUE) {
+        free_algo_md(md);
+      }
+
       hex_digest = get_digest(cmd, algo, path, st.st_mtime, start_pos, len,
         PR_STR_FL_HEX_USE_UC);
       if (hex_digest != NULL) {
@@ -2073,9 +2251,9 @@ MODRET digest_hash(cmd_rec *cmd) {
   }
 
   switch (xerrno) {
-#ifdef EBUSY
+#if defined(EBUSY)
     case EBUSY:
-#endif
+#endif /* EBUSY */
     case EAGAIN:
       /* The HASH draft recommends using 450 for these cases. */
       error_code = R_450;
@@ -2130,19 +2308,27 @@ MODRET digest_opts_hash(cmd_rec *cmd) {
 
   if (strcasecmp(algo_name, "CRC32") == 0) {
     if (digest_algos & DIGEST_ALGO_CRC32) {
+      if (digest_hash_free_md == TRUE) {
+        free_algo_md(digest_hash_md);
+      }
+
       digest_hash_algo = DIGEST_ALGO_CRC32;
-      digest_hash_md = get_algo_md(digest_hash_algo);
+      digest_hash_md = get_algo_md(digest_hash_algo, &digest_hash_free_md);
 
     } else {
       pr_response_add_err(R_501, _("%s: Unsupported algorithm"), algo_name);
       return PR_ERROR(cmd);
     }
 
-#ifndef OPENSSL_NO_MD5
+#if !defined(OPENSSL_NO_MD5)
   } else if (strcasecmp(algo_name, "MD5") == 0) {
     if (digest_algos & DIGEST_ALGO_MD5) {
+      if (digest_hash_free_md == TRUE) {
+        free_algo_md(digest_hash_md);
+      }
+
       digest_hash_algo = DIGEST_ALGO_MD5;
-      digest_hash_md = get_algo_md(digest_hash_algo);
+      digest_hash_md = get_algo_md(digest_hash_algo, &digest_hash_free_md);
 
     } else {
       pr_response_add_err(R_501, _("%s: Unsupported algorithm"), algo_name);
@@ -2150,11 +2336,15 @@ MODRET digest_opts_hash(cmd_rec *cmd) {
     }
 #endif /* OPENSSL_NO_MD5 */
 
-#ifndef OPENSSL_NO_SHA1
+#if !defined(OPENSSL_NO_SHA1)
   } else if (strcasecmp(algo_name, "SHA-1") == 0) {
     if (digest_algos & DIGEST_ALGO_SHA1) {
+      if (digest_hash_free_md == TRUE) {
+        free_algo_md(digest_hash_md);
+      }
+
       digest_hash_algo = DIGEST_ALGO_SHA1;
-      digest_hash_md = get_algo_md(digest_hash_algo);
+      digest_hash_md = get_algo_md(digest_hash_algo, &digest_hash_free_md);
 
     } else {
       pr_response_add_err(R_501, _("%s: Unsupported algorithm"), algo_name);
@@ -2162,11 +2352,15 @@ MODRET digest_opts_hash(cmd_rec *cmd) {
     }
 #endif /* OPENSSL_NO_SHA1 */
 
-#ifndef OPENSSL_NO_SHA256
+#if !defined(OPENSSL_NO_SHA256)
   } else if (strcasecmp(algo_name, "SHA-256") == 0) {
     if (digest_algos & DIGEST_ALGO_SHA256) {
+      if (digest_hash_free_md == TRUE) {
+        free_algo_md(digest_hash_md);
+      }
+
       digest_hash_algo = DIGEST_ALGO_SHA256;
-      digest_hash_md = get_algo_md(digest_hash_algo);
+      digest_hash_md = get_algo_md(digest_hash_algo, &digest_hash_free_md);
 
     } else {
       pr_response_add_err(R_501, _("%s: Unsupported algorithm"), algo_name);
@@ -2174,11 +2368,15 @@ MODRET digest_opts_hash(cmd_rec *cmd) {
     }
 #endif /* OPENSSL_NO_SHA256 */
 
-#ifndef OPENSSL_NO_SHA512
+#if !defined(OPENSSL_NO_SHA512)
   } else if (strcasecmp(algo_name, "SHA-512") == 0) {
     if (digest_algos & DIGEST_ALGO_SHA512) {
+      if (digest_hash_free_md == TRUE) {
+        free_algo_md(digest_hash_md);
+      }
+
       digest_hash_algo = DIGEST_ALGO_SHA512;
-      digest_hash_md = get_algo_md(digest_hash_algo);
+      digest_hash_md = get_algo_md(digest_hash_algo, &digest_hash_free_md);
 
     } else {
       pr_response_add_err(R_501, _("%s: Unsupported algorithm"), algo_name);
@@ -2781,9 +2979,18 @@ static void digest_data_xfer_ev(const void *event_data, void *user_data) {
 
 #if defined(PR_SHARED_MODULE)
 static void digest_mod_unload_ev(const void *event_data, void *user_data) {
-  if (strcmp((char *) event_data, "mod_digest.c") == 0) {
-    pr_event_unregister(&digest_module, NULL, NULL);
+  if (strcmp((char *) event_data, "mod_digest.c") != 0) {
+    return;
   }
+
+  pr_event_unregister(&digest_module, NULL, NULL);
+
+# if OPENSSL_VERSION_NUMBER >= 0x40000000L && !defined(HAVE_LIBRESSL)
+  if (crc32_provider != NULL) {
+    OSSL_PROVIDER_unload(crc32_provider);
+    crc32_provider = NULL;
+  }
+# endif /* OpenSSL 4.x and later */
 }
 #endif /* PR_SHARED_MODULE */
 
@@ -2811,6 +3018,15 @@ static void digest_sess_reinit_ev(const void *event_data, void *user_data) {
   }
 }
 
+static void digest_shutdown_ev(const void *event_data, void *user_data) {
+#if defined(HAVE_OSSL_PROVIDER_LOAD_OPENSSL)
+  if (crc32_provider != NULL) {
+    OSSL_PROVIDER_unload(crc32_provider);
+    crc32_provider = NULL;
+  }
+#endif /* HAVE_OSSL_PROVIDER_LOAD_OPENSSL */
+}
+
 /* Initialization routines
  */
 
@@ -2822,6 +3038,27 @@ static int digest_init(void) {
   pr_event_register(&digest_module, "core.module-unload", digest_mod_unload_ev,
     NULL);
 #endif /* PR_SHARED_MODULE */
+  pr_event_register(&digest_module, "core.shutdown", digest_shutdown_ev, NULL);
+
+#if OPENSSL_VERSION_NUMBER >= 0x40000000L && !defined(HAVE_LIBRESSL)
+  if (OSSL_PROVIDER_add_builtin(NULL, "crc32", crc32_provider_init) != 1) {
+    pr_log_debug(DEBUG1, MOD_DIGEST_VERSION
+      ": error registering 'crc32' OpenSSL provider: %s", get_errors());
+
+  } else {
+    pr_trace_msg(trace_channel, 9, "%s", "registered 'crc32' OpenSSL provider");
+  }
+
+  /* Load our custom "crc32" OpenSSL algorithm provider. */
+  crc32_provider = OSSL_PROVIDER_load(NULL, "crc32");
+  if (crc32_provider == NULL) {
+    pr_log_pri(PR_LOG_NOTICE, MOD_DIGEST_VERSION
+      ": error loading 'crc32' OpenSSL provider: %s", get_errors());
+
+  } else {
+    pr_trace_msg(trace_channel, 9, "%s", "loaded 'crc32' OpenSSL provider");
+  }
+#endif /* OpenSSL 4.x and later */
 
   return 0;
 }
@@ -2893,7 +3130,7 @@ static int digest_sess_init(void) {
     }
   }
 
-  digest_hash_md = get_algo_md(digest_hash_algo);
+  digest_hash_md = get_algo_md(digest_hash_algo, &digest_hash_free_md);
 
   c = find_config(main_server->conf, CONF_PARAM, "DigestCache", FALSE);
   if (c != NULL) {


=====================================
contrib/mod_quotatab.c
=====================================
@@ -1432,6 +1432,73 @@ int quotatab_write(quota_tally_t *tally,
 
 static off_t copied_bytes = 0;
 
+static ssize_t quotatab_fsio_pwrite(pr_fh_t *fh, int fd, const void *buf,
+    size_t bufsz, off_t offset) {
+  ssize_t res;
+  off_t total_bytes;
+
+  res = pwrite(fd, buf, bufsz, offset);
+  if (res < 0) {
+    return res;
+  }
+
+  if (have_quota_update == 0) {
+    return res;
+  }
+
+  /* Check to see if we've exceeded our upload limit.  mod_xfer will
+   * have called pr_data_xfer(), which will have updated
+   * session.xfer.total_bytes, before calling pr_fsio_write(), so
+   * we do not have to worry about updated/changing session.xfer.total_bytes
+   * ourselves.
+   *
+   * Note that there is a race condition here: it is possible for the same
+   * user to be writing to the same file in chunks from multiple
+   * simultaneous connections.
+   */
+
+  /* If the client is copying a file (versus uploading a file), then we need
+   * to track the "total bytes" differently.
+   */
+  if (session.curr_cmd_id == PR_CMD_SITE_ID &&
+      (session.curr_cmd_rec->argc >= 2 &&
+       (strcasecmp(session.curr_cmd_rec->argv[1], "CPTO") == 0 ||
+        strcasecmp(session.curr_cmd_rec->argv[1], "COPY") == 0))) {
+    copied_bytes += res;
+    total_bytes = copied_bytes;
+
+  } else {
+    total_bytes = session.xfer.total_bytes;
+  }
+
+  if (sess_limit.bytes_in_avail > 0.0 &&
+      sess_tally.bytes_in_used + total_bytes > sess_limit.bytes_in_avail) {
+    int xerrno;
+    char *errstr = NULL;
+
+    xerrno = get_quota_exceeded_errno(EIO, &errstr);
+    quotatab_log("quotatab write(): limit exceeded, returning %s", errstr);
+
+    errno = xerrno;
+    return -1;
+  }
+
+  if (sess_limit.bytes_xfer_avail > 0.0 &&
+      sess_tally.bytes_xfer_used + total_bytes > sess_limit.bytes_xfer_avail) {
+    int xerrno;
+    char *errstr = NULL;
+
+    xerrno = get_quota_exceeded_errno(EIO, &errstr);
+    quotatab_log("quotatab write(): transfer limit exceeded, returning %s",
+      errstr);
+
+    errno = xerrno;
+    return -1;
+  }
+
+  return res;
+}
+
 static int quotatab_fsio_write(pr_fh_t *fh, int fd, const char *buf,
     size_t bufsz) {
   int res;
@@ -3214,6 +3281,7 @@ MODRET quotatab_post_pass(cmd_rec *cmd) {
          * For Issue #1764, this is not a problem due to the fact that
          * mod_vroot does not override the system FS write callbacks.
          */
+        fs->pwrite = quotatab_fsio_pwrite;
         fs->write = quotatab_fsio_write;
 
       } else {


=====================================
contrib/mod_sftp/Makefile.in
=====================================
@@ -19,13 +19,14 @@ MODULE_OBJS=mod_sftp.o msg.o packet.o cipher.o mac.o umac.o umac128.o \
   compress.o kex.o keys.o crypto.o utf8.o session.o service.o kbdint.o \
   auth-hostbased.o auth-kbdint.o auth-password.o auth-publickey.o auth.o \
   disconnect.o rfc4716.o keystore.o channel.o blacklist.o agent.o \
-  interop.o tap.o fxp.o scp.o display.o misc.o date.o bcrypt.o poly1305.o
+  interop.o tap.o fxp.o scp.o display.o misc.o date.o bcrypt.o poly1305.o \
+  provider.o
 SHARED_MODULE_OBJS=mod_sftp.lo msg.lo packet.lo cipher.lo mac.lo umac.lo \
   umac128.lo compress.lo kex.lo keys.lo crypto.lo utf8.lo session.lo \
   service.lo kbdint.lo auth-hostbased.lo auth-kbdint.lo auth-password.lo \
   auth-publickey.lo auth.lo disconnect.lo rfc4716.lo keystore.lo channel.lo \
   blacklist.lo agent.lo interop.lo tap.lo fxp.lo scp.lo display.lo misc.lo \
-  date.lo bcrypt.lo poly1305.lo
+  date.lo bcrypt.lo poly1305.lo provider.lo
 
 # Necessary redefinitions
 INCLUDES=-I. -I../.. -I../../include -I$(top_srcdir)/../../include @INCLUDES@


=====================================
contrib/mod_sftp/crypto.c
=====================================
@@ -24,6 +24,7 @@
 
 #include "mod_sftp.h"
 #include "crypto.h"
+#include "provider.h"
 #include "umac.h"
 
 /* In OpenSSL 0.9.7, all des_ functions were renamed to DES_ to avoid
@@ -882,6 +883,9 @@ static const EVP_CIPHER *get_aes_ctr_cipher(int key_len) {
 }
 #endif /* OpenSSL implements AES CTR modes */
 
+#if OPENSSL_VERSION_NUMBER >= 0x40000000L && !defined(HAVE_LIBRESSL)
+/* We'll use the Provider interface for UMAC digests in this case. */
+#else
 static int update_umac64(EVP_MD_CTX *ctx, const void *data, size_t len) {
   int res;
   void *md_data;
@@ -1010,12 +1014,26 @@ static int delete_umac128(EVP_MD_CTX *ctx) {
 
   return 1;
 }
+#endif /* OpenSSL before 4.x */
 
-static const EVP_MD *get_umac64_digest(void) {
-  EVP_MD *md;
+static const EVP_MD *get_umac64_digest(int *free_md) {
+  EVP_MD *md = NULL;
 
-#if (OPENSSL_VERSION_NUMBER >= 0x10100000L && !defined(HAVE_LIBRESSL)) || \
-    (defined(HAVE_LIBRESSL) && LIBRESSL_VERSION_NUMBER >= 0x3050000L)
+  *free_md = FALSE;
+
+#if OPENSSL_VERSION_NUMBER >= 0x40000000L && !defined(HAVE_LIBRESSL)
+  md = EVP_MD_fetch(NULL, "umac64", NULL);
+  if (md == NULL) {
+    pr_trace_msg(trace_channel, 4, "error fetching 'umac64' EVP_MD: %s",
+      sftp_crypto_get_errors());
+
+  } else {
+    *free_md = TRUE;
+  }
+
+#else
+# if (OPENSSL_VERSION_NUMBER >= 0x10100000L && !defined(HAVE_LIBRESSL)) || \
+     (defined(HAVE_LIBRESSL) && LIBRESSL_VERSION_NUMBER >= 0x3050000L)
   /* XXX TODO: At some point, we also need to call EVP_MD_meth_free() on
    * this, to avoid a resource leak.
    */
@@ -1026,7 +1044,7 @@ static const EVP_MD *get_umac64_digest(void) {
   EVP_MD_meth_set_update(md, update_umac64);
   EVP_MD_meth_set_final(md, final_umac64);
   EVP_MD_meth_set_cleanup(md, delete_umac64);
-#else
+# else
   static EVP_MD umac64_digest;
 
   memset(&umac64_digest, 0, sizeof(EVP_MD));
@@ -1041,16 +1059,30 @@ static const EVP_MD *get_umac64_digest(void) {
   umac64_digest.block_size = 32;
 
   md = &umac64_digest;
-#endif /* prior to OpenSSL-1.1.0/LibreSSL-3.5.0 */
+# endif /* prior to OpenSSL-1.1.0/LibreSSL-3.5.0 */
+#endif /* OpenSSL before 4.x */
 
   return md;
 }
 
-static const EVP_MD *get_umac128_digest(void) {
-  EVP_MD *md;
+static const EVP_MD *get_umac128_digest(int *free_md) {
+  EVP_MD *md = NULL;
 
-#if (OPENSSL_VERSION_NUMBER >= 0x10100000L && !defined(HAVE_LIBRESSL)) || \
-    (defined(HAVE_LIBRESSL) && LIBRESSL_VERSION_NUMBER >= 0x3050000L)
+  *free_md = FALSE;
+
+#if OPENSSL_VERSION_NUMBER >= 0x40000000L && !defined(HAVE_LIBRESSL)
+  md = EVP_MD_fetch(NULL, "umac128", NULL);
+  if (md == NULL) {
+    pr_trace_msg(trace_channel, 4, "error fetching 'umac128' EVP_MD: %s",
+      sftp_crypto_get_errors());
+
+  } else {
+    *free_md = TRUE;
+  }
+
+#else
+# if (OPENSSL_VERSION_NUMBER >= 0x10100000L && !defined(HAVE_LIBRESSL)) || \
+     (defined(HAVE_LIBRESSL) && LIBRESSL_VERSION_NUMBER >= 0x3050000L)
   /* XXX TODO: At some point, we also need to call EVP_MD_meth_free() on
    * this, to avoid a resource leak.
    */
@@ -1061,7 +1093,7 @@ static const EVP_MD *get_umac128_digest(void) {
   EVP_MD_meth_set_update(md, update_umac128);
   EVP_MD_meth_set_final(md, final_umac128);
   EVP_MD_meth_set_cleanup(md, delete_umac128);
-#else
+# else
   static EVP_MD umac128_digest;
 
   memset(&umac128_digest, 0, sizeof(EVP_MD));
@@ -1076,7 +1108,8 @@ static const EVP_MD *get_umac128_digest(void) {
   umac128_digest.block_size = 64;
 
   md = &umac128_digest;
-#endif /* prior to OpenSSL-1.1.0/LibreSSL-3.5.0 */
+# endif /* prior to OpenSSL-1.1.0/LibreSSL-3.5.0 */
+#endif /* OpenSSL before 4.x */
 
   return md;
 }
@@ -1183,7 +1216,18 @@ const EVP_CIPHER *sftp_crypto_get_cipher(const char *name, size_t *key_len,
   return NULL;
 }
 
-const EVP_MD *sftp_crypto_get_digest(const char *name, uint32_t *mac_len) {
+void sftp_crypto_free_digest(const EVP_MD *md) {
+#if (OPENSSL_VERSION_NUMBER >= 0x30000000L && !defined(HAVE_LIBRESSL)) || \
+     (defined(HAVE_LIBRESSL) && LIBRESSL_VERSION_NUMBER >= 0x3080000L)
+  EVP_MD_free((EVP_MD *) md);
+#else
+  /* Avoid compiler warnings. */
+  (void) md;
+#endif /* OpenSSL-3.x/LibreSSL-3.8.x and later */
+}
+
+const EVP_MD *sftp_crypto_get_digest(const char *name, uint32_t *mac_len,
+    int *free_md) {
   register unsigned int i;
 
   if (name == NULL) {
@@ -1191,6 +1235,8 @@ const EVP_MD *sftp_crypto_get_digest(const char *name, uint32_t *mac_len) {
     return NULL;
   }
 
+  *free_md = FALSE;
+
   for (i = 0; digests[i].name; i++) {
     if (strcmp(digests[i].name, name) == 0) {
       const EVP_MD *digest = NULL;
@@ -1198,11 +1244,11 @@ const EVP_MD *sftp_crypto_get_digest(const char *name, uint32_t *mac_len) {
 #if OPENSSL_VERSION_NUMBER > 0x000907000L
       if (strcmp(name, "umac-64 at openssh.com") == 0 ||
           strcmp(name, "umac-64-etm at openssh.com") == 0) {
-        digest = get_umac64_digest();
+        digest = get_umac64_digest(free_md);
 
       } else if (strcmp(name, "umac-128 at openssh.com") == 0 ||
                  strcmp(name, "umac-128-etm at openssh.com") == 0) {
-        digest = get_umac128_digest();
+        digest = get_umac128_digest(free_md);
 #else
       if (FALSE) {
 #endif /* OpenSSL older than 0.9.7 */
@@ -1572,7 +1618,16 @@ size_t sftp_crypto_get_size(size_t first, size_t second) {
 #endif /* !roundup */
 }
 
+int sftp_crypto_init(void) {
+  if (sftp_provider_init() < 0) {
+    return -1;
+  }
+
+  return 0;
+}
+
 void sftp_crypto_free(int flags) {
+  sftp_provider_free();
 
   /* Only call EVP_cleanup() et al if other OpenSSL-using modules are not
    * present.  If we called EVP_cleanup() here during a restart,


=====================================
contrib/mod_sftp/crypto.h
=====================================
@@ -1,6 +1,6 @@
 /*
  * ProFTPD - mod_sftp misc crypto routines
- * Copyright (c) 2008-2022 TJ Saunders
+ * Copyright (c) 2008-2026 TJ Saunders
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -27,10 +27,12 @@
 
 #include "mod_sftp.h"
 
+int sftp_crypto_init(void);
 void sftp_crypto_free(int);
 const EVP_CIPHER *sftp_crypto_get_cipher(const char *name, size_t *key_len,
   size_t *auth_len, size_t *discard_len);
-const EVP_MD *sftp_crypto_get_digest(const char *, uint32_t *);
+const EVP_MD *sftp_crypto_get_digest(const char *, uint32_t *, int *);
+void sftp_crypto_free_digest(const EVP_MD *md);
 int sftp_crypto_is_hostkey(const char *name);
 int sftp_crypto_is_key_exchange(const char *name);
 int sftp_crypto_set_driver(const char *);


=====================================
contrib/mod_sftp/fxp.c
=====================================
@@ -281,12 +281,14 @@ static size_t fxp_packet_data_allocsz = 0;
 #define FXP_PACKET_DATA_DEFAULT_SZ		(1024 * 16)
 #define FXP_RESPONSE_DATA_DEFAULT_SZ		512
 
-#ifdef PR_USE_XATTR
+#if defined(PR_USE_XATTR)
+/* Impose limits on the xattr value length we are willing to process. */
+# define FXP_XATTR_VALUE_MAX_LEN		(1024 * 64)
 /* Allocate larger buffers for extended attributes */
 # define FXP_RESPONSE_NAME_DEFAULT_SZ		(1024 * 4)
 #endif /* PR_USE_XATTR */
 
-#ifndef FXP_RESPONSE_NAME_DEFAULT_SZ
+#if !defined(FXP_RESPONSE_NAME_DEFAULT_SZ)
 # define FXP_RESPONSE_NAME_DEFAULT_SZ		FXP_RESPONSE_DATA_DEFAULT_SZ
 #endif
 
@@ -1893,7 +1895,7 @@ static char *fxp_strattrs(pool *p, struct stat *st, uint32_t *attr_flags) {
 
       if (fxp_session->client_version >= 6) {
         flags |= SSH2_FX_ATTR_LINK_COUNT;
-#ifdef PR_USE_XATTR
+#if defined(PR_USE_XATTR)
         flags |= SSH2_FX_ATTR_EXTENDED;
 #endif /* PR_USE_XATTR */
       }
@@ -2572,7 +2574,7 @@ static uint32_t fxp_xattrs_write(pool *p, struct fxp_buffer *fxb,
     const char *path) {
   uint32_t len = 0;
 
-#ifdef PR_USE_XATTR
+#if defined(PR_USE_XATTR)
   int res;
   array_header *names = NULL;
 
@@ -3410,6 +3412,23 @@ static struct fxp_packet *fxp_packet_read(uint32_t channel_id,
       "(%lu bytes remaining in buffer)", (unsigned long) fxp->packet_len,
       (unsigned long) buflen);
 
+    /* We require 5 bytes of SFTP request data at a minimum: 1 byte for the
+     * request type, and 4 bytes for the payload length (Issue #2115).
+     */
+    if (fxp->packet_len < 5) {
+      (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+        "illegal SFTP request length (%lu bytes, require at least 5 bytes), "
+        "rejecting", (unsigned long) fxp->packet_len);
+      SFTP_DISCONNECT_CONN(SFTP_SSH2_DISCONNECT_BY_APPLICATION, NULL);
+    }
+
+    if (fxp->packet_len > FXP_MAX_PACKET_LEN) {
+      (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+        "received excessive SFTP packet (len %lu > max %lu bytes), rejecting",
+        (unsigned long) fxp->packet_len, (unsigned long) FXP_MAX_PACKET_LEN);
+      SFTP_DISCONNECT_CONN(SFTP_SSH2_DISCONNECT_BY_APPLICATION, NULL);
+    }
+
     if (buflen == 0) {
       fxp_packet_set_packet(fxp);
       fxp_packet_clear_cache_data();
@@ -5954,7 +5973,7 @@ static int fxp_handle_ext_statvfs(struct fxp_packet *fxp, const char *path) {
 }
 #endif /* !HAVE_SYS_STATVFS_H */
 
-#ifdef PR_USE_XATTR
+#if defined(PR_USE_XATTR)
 static int fxp_handle_ext_getxattr(struct fxp_packet *fxp, const char *path,
     const char *name, uint32_t valsz) {
   ssize_t res;
@@ -5964,6 +5983,36 @@ static int fxp_handle_ext_getxattr(struct fxp_packet *fxp, const char *path,
   const char *reason;
   struct fxp_packet *resp;
 
+  buflen = bufsz = FXP_RESPONSE_DATA_DEFAULT_SZ;
+  buf = ptr = palloc(fxp->pool, bufsz);
+
+  if (valsz > FXP_XATTR_VALUE_MAX_LEN) {
+    int xerrno = EINVAL;
+
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "getxattr(2) on '%s' for attribute '%s' (%lu bytes) exceeds maximum "
+      "value size (%lu bytes), denying", path, name, (unsigned long) valsz,
+      (unsigned long) FXP_XATTR_VALUE_MAX_LEN);
+
+    status_code = fxp_errno2status(xerrno, &reason);
+
+    pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s' "
+      "('%s' [%d])", (unsigned long) status_code, reason, strerror(xerrno),
+      xerrno);
+
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
+
+    resp = fxp_packet_create(fxp->pool, fxp->channel_id);
+    resp->payload = ptr;
+    resp->payload_sz = (bufsz - buflen);
+
+    return fxp_packet_write(resp);
+  }
+
+  /* Now that the value size has been checked, we allocate a new, larger
+   * buffer for that size.
+   */
   val = pcalloc(fxp->pool, (size_t) valsz+1);
 
   buflen = bufsz = FXP_RESPONSE_DATA_DEFAULT_SZ + valsz;
@@ -6017,7 +6066,7 @@ static int fxp_handle_ext_fgetxattr(struct fxp_packet *fxp, const char *handle,
   struct fxp_handle *fxh;
   struct fxp_packet *resp;
 
-  buflen = bufsz = FXP_RESPONSE_DATA_DEFAULT_SZ + valsz;
+  buflen = bufsz = FXP_RESPONSE_DATA_DEFAULT_SZ;
   buf = ptr = palloc(fxp->pool, bufsz);
 
   fxh = fxp_handle_get(handle);
@@ -6066,6 +6115,37 @@ static int fxp_handle_ext_fgetxattr(struct fxp_packet *fxp, const char *handle,
   }
 
   path = fxh->fh->fh_path;
+
+  if (valsz > FXP_XATTR_VALUE_MAX_LEN) {
+    int xerrno = EINVAL;
+
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "fgetxattr(2) on '%s' for attribute '%s' (%lu bytes) exceeds maximum "
+      "value size (%lu bytes), denying", path, name, (unsigned long) valsz,
+      (unsigned long) FXP_XATTR_VALUE_MAX_LEN);
+
+    status_code = fxp_errno2status(xerrno, &reason);
+
+    pr_trace_msg(trace_channel, 8, "sending response: STATUS %lu '%s' "
+      "('%s' [%d])", (unsigned long) status_code, reason, strerror(xerrno),
+      xerrno);
+
+    fxp_status_write(fxp->pool, &buf, &buflen, fxp->request_id, status_code,
+      reason, NULL);
+
+    resp = fxp_packet_create(fxp->pool, fxp->channel_id);
+    resp->payload = ptr;
+    resp->payload_sz = (bufsz - buflen);
+
+    return fxp_packet_write(resp);
+  }
+
+  /* Now that the value size has been checked, we allocate a new, larger
+   * buffer for that size.
+   */
+  buflen = bufsz = FXP_RESPONSE_DATA_DEFAULT_SZ + valsz;
+  buf = ptr = palloc(fxp->pool, bufsz);
+
   val = pcalloc(fxp->pool, (size_t) valsz+1);
 
   res = pr_fsio_fgetxattr(fxp->pool, fxh->fh, name, val, (size_t) valsz);
@@ -8054,7 +8134,7 @@ static int fxp_handle_fstat(struct fxp_packet *fxp) {
     pr_trace_msg(trace_channel, 7, "received request: FSTAT %s", name);
     attr_flags = SSH2_FX_ATTR_SIZE|SSH2_FX_ATTR_UIDGID|SSH2_FX_ATTR_PERMISSIONS|
       SSH2_FX_ATTR_ACMODTIME;
-#ifdef PR_USE_XATTR
+#if defined(PR_USE_XATTR)
     if (!(fxp_fsio_opts & PR_FSIO_OPT_IGNORE_XATTR)) {
       attr_flags |= SSH2_FX_ATTR_EXTENDED;
     }
@@ -8787,7 +8867,7 @@ static int fxp_handle_lstat(struct fxp_packet *fxp) {
     pr_trace_msg(trace_channel, 7, "received request: LSTAT %s", path);
     attr_flags = SSH2_FX_ATTR_SIZE|SSH2_FX_ATTR_UIDGID|SSH2_FX_ATTR_PERMISSIONS|
       SSH2_FX_ATTR_ACMODTIME;
-#ifdef PR_USE_XATTR
+#if defined(PR_USE_XATTR)
     if (!(fxp_fsio_opts & PR_FSIO_OPT_IGNORE_XATTR)) {
       attr_flags |= SSH2_FX_ATTR_EXTENDED;
     }
@@ -10349,7 +10429,7 @@ static int fxp_handle_read(struct fxp_packet *fxp) {
   unsigned char *buf, *data = NULL, *ptr;
   char *file, *name, *ptr2;
   ssize_t res;
-  uint32_t buflen, bufsz, datalen;
+  uint32_t buflen, bufsz, datalen, max_readsz;
   uint64_t offset;
   struct fxp_handle *fxh;
   struct fxp_packet *resp;
@@ -10360,17 +10440,17 @@ static int fxp_handle_read(struct fxp_packet *fxp) {
   offset = sftp_msg_read_long(fxp->pool, &fxp->payload, &fxp->payload_sz);
   datalen = sftp_msg_read_int(fxp->pool, &fxp->payload, &fxp->payload_sz);
 
-#if 0
-  /* XXX This doesn't appear to be needed now.  But I'll keep it around,
-   * just in case some buggy client needs this treatment.
+  /* We tell clients that request the "limits at openssh.com" extension what
+   * the maximum allowed READ length is; we should enforce that here.
    */
+  max_readsz = FXP_MAX_PACKET_LEN - 1024;
+
   if (datalen > max_readsz) {
     pr_trace_msg(trace_channel, 8,
       "READ requested len %lu exceeds max (%lu), truncating",
       (unsigned long) datalen, (unsigned long) max_readsz);
     datalen = max_readsz;
   }
-#endif
 
   cmd = fxp_cmd_alloc(fxp->pool, "READ", name);
   cmd->cmd_class = CL_READ|CL_SFTP;
@@ -10844,7 +10924,7 @@ static int fxp_handle_readdir(struct fxp_packet *fxp) {
     pr_signals_handle();
 
     /* How much non-path data do we expect to be associated with this entry? */
-#ifdef PR_USE_XATTR
+#if defined(PR_USE_XATTR)
     /* Note that the "extra space" to allocate for extended attributes is
      * currently a bit of a guess.  Initially, this was 4K; that was causing
      * slower directory listings due to the need for more READDIR requests,
@@ -10996,7 +11076,7 @@ static int fxp_handle_readdir(struct fxp_packet *fxp) {
      * Thus we CHOOSE to only provide these extended attributes, if supported,
      * to protocol version 6 clients.
      */
-#ifdef PR_USE_XATTR
+#if defined(PR_USE_XATTR)
     if (!(fxp_fsio_opts & PR_FSIO_OPT_IGNORE_XATTR)) {
       attr_flags |= SSH2_FX_ATTR_EXTENDED;
     }
@@ -12873,7 +12953,7 @@ static int fxp_handle_stat(struct fxp_packet *fxp) {
     pr_trace_msg(trace_channel, 7, "received request: STAT %s", path);
     attr_flags = SSH2_FX_ATTR_SIZE|SSH2_FX_ATTR_UIDGID|SSH2_FX_ATTR_PERMISSIONS|
       SSH2_FX_ATTR_ACMODTIME;
-#ifdef PR_USE_XATTR
+#if defined(PR_USE_XATTR)
     if (!(fxp_fsio_opts & PR_FSIO_OPT_IGNORE_XATTR)) {
       attr_flags |= SSH2_FX_ATTR_EXTENDED;
     }
@@ -14052,15 +14132,6 @@ int sftp_fxp_handle_packet(pool *p, void *ssh2, uint32_t channel_id,
         (unsigned long) channel_id);
     }
 
-    if (fxp->packet_len > FXP_MAX_PACKET_LEN) {
-      (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
-        "received excessive SFTP packet (len %lu > max %lu bytes), rejecting",
-        (unsigned long) fxp->packet_len, (unsigned long) FXP_MAX_PACKET_LEN);
-      destroy_pool(fxp->pool);
-      errno = EPERM;
-      return -1;
-    }
-
     fxp_session = fxp_get_session(channel_id);
     if (fxp_session == NULL) {
       (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,


=====================================
contrib/mod_sftp/kex.c
=====================================
@@ -892,7 +892,7 @@ static int have_good_dh(DH *dh, const BIGNUM *pub_key) {
 }
 
 static int get_dh_nbits(struct sftp_kex *kex) {
-  int dh_nbits = 0, dh_size = 0;
+  int dh_nbits = 0, dh_size = 0, free_digest = FALSE;
   const char *algo;
   const EVP_CIPHER *cipher;
   const EVP_MD *digest;
@@ -952,7 +952,7 @@ static int get_dh_nbits(struct sftp_kex *kex) {
   }
 
   algo = kex->session_names->c2s_mac_algo;
-  digest = sftp_crypto_get_digest(algo, NULL);
+  digest = sftp_crypto_get_digest(algo, NULL, &free_digest);
   if (digest != NULL) {
     int mac_len;
 
@@ -963,10 +963,14 @@ static int get_dh_nbits(struct sftp_kex *kex) {
         "set DH size to %d bytes, matching client-to-server '%s' digest size",
         dh_size, algo);
     }
+
+    if (free_digest == TRUE) {
+      sftp_crypto_free_digest(digest);
+    }
   }
 
   algo = kex->session_names->s2c_mac_algo;
-  digest = sftp_crypto_get_digest(algo, NULL);
+  digest = sftp_crypto_get_digest(algo, NULL, &free_digest);
   if (digest != NULL) {
     int mac_len;
 
@@ -977,6 +981,10 @@ static int get_dh_nbits(struct sftp_kex *kex) {
         "set DH size to %d bytes, matching server-to-client '%s' digest size",
         dh_size, algo);
     }
+
+    if (free_digest == TRUE) {
+      sftp_crypto_free_digest(digest);
+    }
   }
 
   /* We want to return bits, not bytes. */


=====================================
contrib/mod_sftp/keys.c
=====================================
@@ -5337,6 +5337,12 @@ static int verify_rsa_signed_data(pool *p, EVP_PKEY *pkey,
   }
 
   rsa = EVP_PKEY_get1_RSA(pkey);
+  if (rsa == NULL) {
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "error obtaining RSA key: %s",  sftp_crypto_get_errors());
+    errno = EINVAL;
+    return -1;
+  }
 
   if (keys_rsa_min_nbits > 0) {
     int rsa_nbits;
@@ -5471,6 +5477,8 @@ static int dsa_verify_signed_data(pool *p, EVP_PKEY *pkey,
   if (sig_len != 40) {
     (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
       "bad DSA signature len (%lu)", (unsigned long) sig_len);
+    errno = EINVAL;
+    return -1;
   }
 
   len = sftp_msg_read_data2(p, &signature, &signature_len, sig_len, &sig);
@@ -5487,6 +5495,12 @@ static int dsa_verify_signed_data(pool *p, EVP_PKEY *pkey,
   }
 
   dsa = EVP_PKEY_get1_DSA(pkey);
+  if (dsa == NULL) {
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "error obtaining DSA key: %s",  sftp_crypto_get_errors());
+    errno = EINVAL;
+    return -1;
+  }
 
   if (keys_dsa_min_nbits > 0) {
     int dsa_nbits;
@@ -6175,6 +6189,14 @@ int sftp_keys_verify_signed_data(pool *p, const char *pubkey_algo,
   }
 
   if (strcmp(sig_type, "ssh-rsa") == 0) {
+    if (strcmp(pubkey_algo, sig_type) != 0) {
+      (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+        "unable to verify signed data: signature type '%s' does not match "
+        "publickey algorithm '%s'", sig_type, pubkey_algo);
+      errno = EINVAL;
+      return -1;
+    }
+
     res = rsa_verify_signed_data(p, pkey, signature, signature_len, sig_data,
       sig_datalen);
 
@@ -6210,6 +6232,14 @@ int sftp_keys_verify_signed_data(pool *p, const char *pubkey_algo,
 
 #if !defined(OPENSSL_NO_DSA)
   } else if (strcmp(sig_type, "ssh-dss") == 0) {
+    if (strcmp(pubkey_algo, sig_type) != 0) {
+      (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+        "unable to verify signed data: signature type '%s' does not match "
+        "publickey algorithm '%s'", sig_type, pubkey_algo);
+      errno = EINVAL;
+      return -1;
+    }
+
     res = dsa_verify_signed_data(p, pkey, signature, signature_len, sig_data,
       sig_datalen);
 #endif /* !OPENSSL_NO_DSA */


=====================================
contrib/mod_sftp/mac.c
=====================================
@@ -39,6 +39,7 @@ struct sftp_mac {
   const char *algo;
   unsigned int algo_type;
   int is_etm;
+  int free_digest;
 
   const EVP_MD *digest;
 
@@ -68,15 +69,15 @@ struct sftp_mac {
  */
 
 static struct sftp_mac read_macs[] = {
-  { NULL, NULL, 0, FALSE, NULL, NULL, 0, 0, 0 },
-  { NULL, NULL, 0, FALSE, NULL, NULL, 0, 0, 0 }
+  { NULL, NULL, 0, FALSE, FALSE, NULL, NULL, 0, 0, 0 },
+  { NULL, NULL, 0, FALSE, FALSE, NULL, NULL, 0, 0, 0 }
 };
 static HMAC_CTX *hmac_read_ctxs[2];
 static struct umac_ctx *umac_read_ctxs[2];
 
 static struct sftp_mac write_macs[] = {
-  { NULL, NULL, 0, FALSE, NULL, NULL, 0, 0, 0 },
-  { NULL, NULL, 0, FALSE, NULL, NULL, 0, 0, 0 }
+  { NULL, NULL, 0, FALSE, FALSE, NULL, NULL, 0, 0, 0 },
+  { NULL, NULL, 0, FALSE, FALSE, NULL, NULL, 0, 0, 0 }
 };
 static HMAC_CTX *hmac_write_ctxs[2];
 static struct umac_ctx *umac_write_ctxs[2];
@@ -754,16 +755,25 @@ int sftp_mac_set_read_algo(const char *algo) {
       case SFTP_MAC_ALGO_TYPE_UMAC64:
         umac_delete(umac_read_ctxs[idx]);
         umac_read_ctxs[idx] = NULL;
+        if (read_macs[idx].free_digest == TRUE) {
+          sftp_crypto_free_digest(read_macs[idx].digest);
+          read_macs[idx].digest = NULL;
+        }
         break;
 
       case SFTP_MAC_ALGO_TYPE_UMAC128:
         umac128_delete(umac_read_ctxs[idx]);
         umac_read_ctxs[idx] = NULL;
+        if (read_macs[idx].free_digest == TRUE) {
+          sftp_crypto_free_digest(read_macs[idx].digest);
+          read_macs[idx].digest = NULL;
+        }
         break;
     }
   }
 
-  read_macs[idx].digest = sftp_crypto_get_digest(algo, &mac_len);
+  read_macs[idx].digest = sftp_crypto_get_digest(algo, &mac_len,
+    &(read_macs[idx].free_digest));
   if (read_macs[idx].digest == NULL) {
     return -1;
   }
@@ -928,16 +938,25 @@ int sftp_mac_set_write_algo(const char *algo) {
       case SFTP_MAC_ALGO_TYPE_UMAC64:
         umac_delete(umac_write_ctxs[idx]);
         umac_write_ctxs[idx] = NULL;
+        if (write_macs[idx].free_digest == TRUE) {
+          sftp_crypto_free_digest(write_macs[idx].digest);
+          write_macs[idx].digest = NULL;
+        }
         break;
 
       case SFTP_MAC_ALGO_TYPE_UMAC128:
         umac128_delete(umac_write_ctxs[idx]);
         umac_write_ctxs[idx] = NULL;
+        if (write_macs[idx].free_digest == TRUE) {
+          sftp_crypto_free_digest(write_macs[idx].digest);
+          write_macs[idx].digest = NULL;
+        }
         break;
     }
   }
 
-  write_macs[idx].digest = sftp_crypto_get_digest(algo, &mac_len);
+  write_macs[idx].digest = sftp_crypto_get_digest(algo, &mac_len,
+    &(write_macs[idx].free_digest));
   if (write_macs[idx].digest == NULL) {
     return -1;
   }


=====================================
contrib/mod_sftp/mod_sftp.c
=====================================
@@ -895,14 +895,21 @@ MODRET set_sftpclientmatch(cmd_rec *cmd) {
 
       digests = create_config(c->pool, "SFTPDigests", algos->nelts);
       for (j = 0; j < algos->nelts; j++) {
+        const EVP_MD *md;
         const char *algo;
+        int free_md = FALSE;
 
         algo = ((char **) algos->elts)[j];
-        if (sftp_crypto_get_digest(algo, NULL) == NULL) {
+        md = sftp_crypto_get_digest(algo, NULL, &free_md);
+        if (md == NULL) {
           CONF_ERROR(cmd, pstrcat(cmd->tmp_pool,
             "unsupported digest algorithm: ", algo, NULL));
         }
 
+        if (free_md == TRUE) {
+          sftp_crypto_free_digest(md);
+        }
+
         digests->argv[j] = pstrdup(digests->pool, algo);
       }
 
@@ -1285,10 +1292,18 @@ MODRET set_sftpdigests(cmd_rec *cmd) {
   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
 
   for (i = 1; i < cmd->argc; i++) {
-    if (sftp_crypto_get_digest(cmd->argv[i], NULL) == NULL) {
+    const EVP_MD *md;
+    int free_md = FALSE;
+
+    md = sftp_crypto_get_digest(cmd->argv[i], NULL, &free_md);
+    if (md == NULL) {
       CONF_ERROR(cmd, pstrcat(cmd->tmp_pool,
         "unsupported digest algorithm: ", cmd->argv[i], NULL));
     }
+
+    if (free_md == TRUE) {
+      sftp_crypto_free_digest(md);
+    }
   }
 
   c = add_config_param(cmd->argv[0], cmd->argc-1, NULL);
@@ -2580,6 +2595,7 @@ static int sftp_init(void) {
 #endif /* HAVE_OSSL_PROVIDER_LOAD_OPENSSL */
 
   sftp_keystore_init();
+  sftp_crypto_init();
   sftp_cipher_init();
   sftp_mac_init();
 


=====================================
contrib/mod_sftp/packet.c
=====================================
@@ -89,6 +89,21 @@ static unsigned int client_alive_interval = 0;
 static const char *trace_channel = "ssh2";
 static const char *timing_channel = "timing";
 
+/* This is admittedly an arbitrary upper limit on the number of EXT_INFO
+ * extensions we will handle.
+ *
+ * draft-ssh-ext-info-05 currently defines five:
+ *
+ *  server-sig-algs
+ *  no-flow-control
+ *  accept-channels
+ *  elevation
+ *  delay-compression
+ *
+ * And some implementations, like OpenSSH, will have their own namespaced
+ * extensions.
+ */
+#define MAX_EXT_INFO_COUNT	32
 #define MAX_POLL_TIMEOUTS	3
 
 static int packet_poll(int sockfd, int io) {
@@ -1136,6 +1151,15 @@ int sftp_ssh2_packet_read(int sockfd, struct ssh2_packet *pkt) {
       pr_trace_msg(trace_channel, 20, "SSH2 packet padding len = %u bytes",
         (unsigned int) pkt->padding_len);
 
+      if (pkt->packet_len < (pkt->padding_len + 1)) {
+        (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+          "illegal padding length (%u bytes) exceeds packet length "
+          "(%lu bytes)", (unsigned int) pkt->padding_len,
+          (unsigned long) pkt->packet_len);
+        read_packet_discard(sockfd);
+        return -1;
+      }
+
       pkt->payload_len = (pkt->packet_len - pkt->padding_len - 1);
       pr_trace_msg(trace_channel, 20, "SSH2 packet payload len = %lu bytes",
         (unsigned long) pkt->payload_len);
@@ -1794,6 +1818,14 @@ void sftp_ssh2_packet_handle_ext_info(struct ssh2_packet *pkt) {
   pr_trace_msg(trace_channel, 9, "client sent EXT_INFO with %lu %s",
     (unsigned long) ext_count, ext_count != 1 ? "extensions" : "extension");
 
+  if (ext_count > MAX_EXT_INFO_COUNT) {
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "client sent too many EXT_INFO extensions (%lu, max %lu), ignoring",
+      (unsigned long) ext_count, (unsigned long) MAX_EXT_INFO_COUNT);
+    destroy_pool(pkt->pool);
+    return;
+  }
+
   for (i = 0; i < ext_count; i++) {
     char *ext_name = NULL;
     uint32_t ext_datalen = 0;
@@ -1947,13 +1979,13 @@ static int handle_ssh2_packet(void *data) {
           !(sftp_sess_state & SFTP_SESS_STATE_HAVE_EXT_INFO)) {
         sftp_ssh2_packet_handle_ext_info(pkt);
         sftp_sess_state |= SFTP_SESS_STATE_HAVE_EXT_INFO;
-        break;
 
       } else {
         (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
           "unable to handle %s (%d) message: wrong message order",
           sftp_ssh2_packet_get_msg_type_desc(msg_type), msg_type);
       }
+      break;
 
     case SFTP_SSH2_MSG_SERVICE_REQUEST:
       if (sftp_sess_state & SFTP_SESS_STATE_HAVE_KEX) {
@@ -1962,13 +1994,13 @@ static int handle_ssh2_packet(void *data) {
         }
 
         sftp_sess_state |= SFTP_SESS_STATE_HAVE_SERVICE;
-        break;
 
       } else {
         (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
           "unable to handle %s (%d) message: Key exchange required",
           sftp_ssh2_packet_get_msg_type_desc(msg_type), msg_type);
       }
+      break;
 
     case SFTP_SSH2_MSG_USER_AUTH_REQUEST:
       if (sftp_sess_state & SFTP_SESS_STATE_HAVE_SERVICE) {
@@ -1994,13 +2026,12 @@ static int handle_ssh2_packet(void *data) {
           }
         }
 
-        break;
-
       } else {
         (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
           "unable to handle %s (%d) message: Service request required",
           sftp_ssh2_packet_get_msg_type_desc(msg_type), msg_type);
       }
+      break;
 
     case SFTP_SSH2_MSG_CHANNEL_OPEN:
     case SFTP_SSH2_MSG_CHANNEL_REQUEST:
@@ -2013,13 +2044,12 @@ static int handle_ssh2_packet(void *data) {
           SFTP_DISCONNECT_CONN(SFTP_SSH2_DISCONNECT_BY_APPLICATION, NULL);
         }
 
-        break;
-
       } else {
         (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
           "unable to handle %s (%d) message: User authentication required",
           sftp_ssh2_packet_get_msg_type_desc(msg_type), msg_type);
       }
+      break;
 
     default:
       handle_unknown_msg(pkt, msg_type);
@@ -2042,6 +2072,13 @@ int sftp_ssh2_packet_process(pool *p) {
   pr_response_clear(&resp_err_list);
   pr_response_set_pool(pkt->pool);
 
+  if (pkt->payload_len == 0 ||
+      pkt->payload == NULL) {
+    (void) pr_log_writefile(sftp_logfd, MOD_SFTP_VERSION,
+      "received illegal packet with no payload, disconnecting");
+    SFTP_DISCONNECT_CONN(SFTP_SSH2_DISCONNECT_BY_APPLICATION, NULL);
+  }
+
   /* If a custom handler rejects this packet with ENOSYS, it means we need
    * to fall back to handling it ourselves.  Our own handler never returns
    * ENOSYS.


=====================================
contrib/mod_sftp/provider.c
=====================================
@@ -0,0 +1,313 @@
+/*
+ * ProFTPD - mod_sftp OpenSSL provider
+ * Copyright (c) 2026 TJ Saunders
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses/>.
+ *
+ * As a special exemption, TJ Saunders and other respective copyright holders
+ * give permission to link this program with OpenSSL, and distribute the
+ * resulting executable, without including the source code for OpenSSL in the
+ * source distribution.
+ */
+
+#include "mod_sftp.h"
+#include "crypto.h"
+#include "provider.h"
+#include "umac.h"
+
+#if OPENSSL_VERSION_NUMBER >= 0x40000000L && !defined(HAVE_LIBRESSL)
+# include <openssl/core.h>
+# include <openssl/core_dispatch.h>
+# include <openssl/core_names.h>
+# include <openssl/params.h>
+# include <openssl/provider.h>
+
+static OSSL_PROVIDER *umac_provider = NULL;
+
+static const char *trace_channel = "ssh2";
+
+/* Our custom algorithm provider implementation. */
+
+/* UMAC */
+
+typedef struct umac_ctx_st {
+  struct umac_ctx *umac;
+} UMAC_CTX;
+
+static void *umac_ctx_new(void *vctx) {
+  UMAC_CTX *ctx;
+
+  ctx = OPENSSL_zalloc(sizeof(UMAC_CTX));
+  return ctx;
+}
+
+static void umac_ctx_free(void *vctx) {
+  UMAC_CTX *ctx;
+
+  ctx = vctx;
+  if (ctx->umac != NULL) {
+    umac_delete(ctx->umac);
+    ctx->umac = NULL;
+  }
+
+  OPENSSL_free(ctx);
+}
+
+/* The Provider interface for digests expects an "init" callback, even though
+ * it is not functionally needed for our situation.
+ */
+static int umac_md_init(void *vctx) {
+  (void) vctx;
+
+  return 1;
+}
+
+static const OSSL_PARAM umac_params[] = {
+  OSSL_PARAM_size_t(OSSL_DIGEST_PARAM_BLOCK_SIZE, NULL),
+  OSSL_PARAM_size_t(OSSL_DIGEST_PARAM_SIZE, NULL),
+  OSSL_PARAM_END
+};
+
+/* UMAC64 */
+
+static int umac64_md_update(void *vctx, const unsigned char *data, size_t len) {
+  UMAC_CTX *ctx;
+  struct umac_ctx *umac;
+
+  ctx = vctx;
+  umac = ctx->umac;
+
+  /* The allocation of the umac_ctx is deliberately delayed until the first
+   * update, since the computation of keys depends on the initial bytes
+   * provided.
+   */
+  if (umac == NULL) {
+    umac = umac_new((unsigned char *) data);
+    if (umac == NULL) {
+      return 0;
+    }
+
+    ctx->umac = umac;
+    return 1;
+  }
+
+  return umac_update(umac, (unsigned char *) data, (long) len);
+}
+
+static int umac64_md_final(void *vctx, unsigned char *out, size_t *out_len,
+    size_t outsz) {
+  int res = 1;
+  struct umac_ctx *ctx;
+  unsigned char nonce[8];
+
+  ctx = vctx;
+
+  *out_len = outsz;
+
+  if (outsz != 0) {
+    res = umac_final(ctx, out, nonce);
+  }
+
+  return res;
+}
+
+static int umac64_get_params(void *provctx, OSSL_PARAM params[]) {
+  OSSL_PARAM *p;
+  int ok = 1;
+
+  p = OSSL_PARAM_locate(params, OSSL_DIGEST_PARAM_BLOCK_SIZE);
+  if (p != NULL) {
+    if (OSSL_PARAM_set_size_t(p, 32) != 1) {
+      ok = 0;
+    }
+  }
+
+  if (ok == 1) {
+    p = OSSL_PARAM_locate(params, OSSL_DIGEST_PARAM_SIZE);
+    if (p != NULL) {
+      if (OSSL_PARAM_set_size_t(p, 8) != 1) {
+        ok = 0;
+      }
+    }
+  }
+
+  return ok;
+}
+
+static const OSSL_PARAM *umac64_gettable_params(void) {
+  return umac_params;
+}
+
+static const OSSL_DISPATCH umac64_functions[] = {
+  { OSSL_FUNC_DIGEST_NEWCTX, (void (*)(void)) umac_ctx_new },
+  { OSSL_FUNC_DIGEST_FREECTX, (void (*)(void)) umac_ctx_free },
+  { OSSL_FUNC_DIGEST_INIT, (void (*)(void)) umac_md_init },
+  { OSSL_FUNC_DIGEST_UPDATE, (void (*)(void)) umac64_md_update },
+  { OSSL_FUNC_DIGEST_FINAL, (void (*)(void)) umac64_md_final },
+  { OSSL_FUNC_DIGEST_GET_PARAMS, (void (*)(void)) umac64_get_params },
+  { OSSL_FUNC_DIGEST_GETTABLE_PARAMS, (void (*)(void)) umac64_gettable_params },
+
+  { 0, NULL }
+};
+
+/* UMAC128 */
+
+static int umac128_md_update(void *vctx, const unsigned char *data,
+    size_t len) {
+  UMAC_CTX *ctx;
+  struct umac_ctx *umac;
+
+  ctx = vctx;
+  umac = ctx->umac;
+
+  /* The allocation of the umac_ctx is deliberately delayed until the first
+   * update, since the computation of keys depends on the initial bytes
+   * provided.
+   */
+  if (umac == NULL) {
+    umac = umac128_new((unsigned char *) data);
+    if (umac == NULL) {
+      return 0;
+    }
+
+    ctx->umac = umac;
+    return 1;
+  }
+
+  return umac128_update(umac, (unsigned char *) data, (long) len);
+}
+
+static int umac128_md_final(void *vctx, unsigned char *out, size_t *out_len,
+    size_t outsz) {
+  int res = 1;
+  struct umac_ctx *ctx;
+  unsigned char nonce[8];
+
+  ctx = vctx;
+
+  *out_len = outsz;
+
+  if (outsz != 0) {
+    res = umac128_final(ctx, out, nonce);
+  }
+
+  return res;
+}
+
+static int umac128_get_params(void *provctx, OSSL_PARAM params[]) {
+  OSSL_PARAM *p;
+  int ok = 1;
+
+  p = OSSL_PARAM_locate(params, OSSL_DIGEST_PARAM_BLOCK_SIZE);
+  if (p != NULL) {
+    if (OSSL_PARAM_set_size_t(p, 64) != 1) {
+      ok = 0;
+    }
+  }
+
+  if (ok == 1) {
+    p = OSSL_PARAM_locate(params, OSSL_DIGEST_PARAM_SIZE);
+    if (p != NULL) {
+      if (OSSL_PARAM_set_size_t(p, 16) != 1) {
+        ok = 0;
+      }
+    }
+  }
+
+  return ok;
+}
+
+static const OSSL_PARAM *umac128_gettable_params(void) {
+  return umac_params;
+}
+
+static const OSSL_DISPATCH umac128_functions[] = {
+  { OSSL_FUNC_DIGEST_NEWCTX, (void (*)(void)) umac_ctx_new },
+  { OSSL_FUNC_DIGEST_FREECTX, (void (*)(void)) umac_ctx_free },
+  { OSSL_FUNC_DIGEST_INIT, (void (*)(void)) umac_md_init },
+  { OSSL_FUNC_DIGEST_UPDATE, (void (*)(void)) umac128_md_update },
+  { OSSL_FUNC_DIGEST_FINAL, (void (*)(void)) umac128_md_final },
+  { OSSL_FUNC_DIGEST_GET_PARAMS, (void (*)(void)) umac128_get_params },
+  { OSSL_FUNC_DIGEST_GETTABLE_PARAMS, (void (*)(void)) umac128_gettable_params },
+
+  { 0, NULL }
+};
+
+static const OSSL_ALGORITHM umac_digests[] = {
+  { "umac64", NULL, umac64_functions },
+  { "umac128", NULL, umac128_functions },
+
+  { NULL, NULL, NULL }
+};
+
+static const OSSL_ALGORITHM *umac_provider_operations(void *provctx,
+    int operation_id, int *no_cache) {
+  *no_cache = 0;
+
+  if (operation_id == OSSL_OP_DIGEST) {
+    return umac_digests;
+  }
+
+  return NULL;
+}
+
+static const OSSL_DISPATCH umac_provider_functions[] = {
+  { OSSL_FUNC_PROVIDER_QUERY_OPERATION, (void (*)(void)) umac_provider_operations },
+
+  { 0, NULL }
+};
+
+static int umac_provider_init(const OSSL_CORE_HANDLE *core,
+    const OSSL_DISPATCH *in, const OSSL_DISPATCH **out, void **provctx) {
+  *out = umac_provider_functions;
+  *provctx = (void *) core;
+
+  return 1;
+}
+
+#endif /* OpenSSL 4.x and later */
+
+int sftp_provider_init(void) {
+#if OPENSSL_VERSION_NUMBER >= 0x40000000L && !defined(HAVE_LIBRESSL)
+  if (OSSL_PROVIDER_add_builtin(NULL, "umac", umac_provider_init) != 1) {
+    pr_log_debug(DEBUG1, MOD_SFTP_VERSION
+      ": error registering 'umac' OpenSSL provider: %s",
+      sftp_crypto_get_errors());
+
+  } else {
+    pr_trace_msg(trace_channel, 9, "%s", "registered 'umac' OpenSSL provider");
+  }
+
+  /* Load our custom OpenSSL algorithm provider. */
+  umac_provider = OSSL_PROVIDER_load(NULL, "umac");
+  if (umac_provider == NULL) {
+    pr_log_pri(PR_LOG_NOTICE, MOD_SFTP_VERSION
+      ": error loading 'umac' OpenSSL provider: %s", sftp_crypto_get_errors());
+ 
+  } else {
+    pr_trace_msg(trace_channel, 9, "%s", "loaded 'umac' OpenSSL provider");
+  }
+#endif /* OpenSSL 4.x and later */
+
+  return 0;
+}
+
+void sftp_provider_free(void) {
+#if OPENSSL_VERSION_NUMBER >= 0x40000000L && !defined(HAVE_LIBRESSL)
+  if (umac_provider != NULL) { 
+    OSSL_PROVIDER_unload(umac_provider);
+    umac_provider = NULL;
+  }
+#endif /* OpenSSL 4.x and later */
+}


=====================================
contrib/mod_sftp/provider.h
=====================================
@@ -0,0 +1,32 @@
+/*
+ * ProFTPD - mod_sftp OpenSSL provider
+ * Copyright (c) 2026 TJ Saunders
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses/>.
+ *
+ * As a special exemption, TJ Saunders and other respective copyright holders
+ * give permission to link this program with OpenSSL, and distribute the
+ * resulting executable, without including the source code for OpenSSL in the
+ * source distribution.
+ */
+
+#ifndef MOD_SFTP_PROVIDER_H
+#define MOD_SFTP_PROVIDER_H
+
+#include "mod_sftp.h"
+
+int sftp_provider_init(void);
+void sftp_provider_free(void);
+
+#endif /* MOD_SFTP_PROVIDER_H */


=====================================
contrib/mod_sql.c
=====================================
@@ -984,9 +984,11 @@ static int sql_resolve_on_meta(pool *p, pr_jot_ctx_t *jot_ctx,
       case LOGFMT_META_COMMAND:
       case LOGFMT_META_DIR_NAME:
       case LOGFMT_META_DIR_PATH:
+      case LOGFMT_META_ENV_VAR:
       case LOGFMT_META_FILENAME:
       case LOGFMT_META_IDENT_USER:
       case LOGFMT_META_METHOD:
+      case LOGFMT_META_NOTE_VAR:
       case LOGFMT_META_ORIGINAL_USER:
       case LOGFMT_META_RESPONSE_STR:
       case LOGFMT_META_REMOTE_HOST:
@@ -1009,14 +1011,12 @@ static int sql_resolve_on_meta(pool *p, pr_jot_ctx_t *jot_ctx,
       }
 
       case LOGFMT_META_CLASS:
-      case LOGFMT_META_ENV_VAR:
       case LOGFMT_META_EOS_REASON:
       case LOGFMT_META_GROUP:
       case LOGFMT_META_ISO8601:
       case LOGFMT_META_LOCAL_FQDN:
       case LOGFMT_META_LOCAL_IP:
       case LOGFMT_META_LOCAL_NAME:
-      case LOGFMT_META_NOTE_VAR:
       case LOGFMT_META_PROTOCOL:
       case LOGFMT_META_REMOTE_IP:
       case LOGFMT_META_VERSION:
@@ -1999,8 +1999,11 @@ static struct passwd *sql_getpasswd(cmd_rec *cmd, struct passwd *p) {
       }
 
     } else {
+      /* The username has been escaped according to the backend database' rules
+       * at this point.
+       */
       mr = sql_lookup(sql_make_cmd(cmd->tmp_pool, 3, MOD_SQL_DEF_CONN_NAME,
-        cmap.usercustom, realname ? realname : "NULL"));
+        cmap.usercustom, username ? username : "NULL"));
 
       if (check_response(mr, 0) < 0) {
         return NULL;


=====================================
contrib/mod_tls.c
=====================================
@@ -794,12 +794,12 @@ static int tls_ssl_set_all(server_rec *, SSL *);
 static int tls_openlog(void);
 static int tls_seed_prng(void);
 static int tls_sess_init(void);
-static void tls_setup_environ(pool *, SSL *);
-static void tls_setup_notes(pool *, SSL *);
-static int tls_verify_cb(int, X509_STORE_CTX *);
-static int tls_verify_crl(int, X509_STORE_CTX *);
-static int tls_verify_ocsp(int, X509_STORE_CTX *);
-static char *tls_x509_name_oneline(X509_NAME *);
+static void tls_setup_environ(pool *p, SSL *ssl);
+static void tls_setup_notes(pool *p, SSL *ssl);
+static int tls_verify_cb(int ok, X509_STORE_CTX *ctx);
+static int tls_verify_crl(int ok, X509_STORE_CTX *ctx);
+static int tls_verify_ocsp(int ok, X509_STORE_CTX *ctx);
+static char *tls_x509_name_oneline(const X509_NAME *x509_name);
 
 static int tls_readmore(int);
 static int tls_writemore(int);
@@ -2754,9 +2754,9 @@ static int tls_cert_match_ip_san(pool *p, X509 *cert, const char *ipstr) {
 
 static char *tls_get_cert_cn(pool *p, X509 *cert) {
   int idx = -1;
-  X509_NAME *subj_name = NULL;
-  X509_NAME_ENTRY *cn_entry = NULL;
-  ASN1_STRING *cn_asn1 = NULL;
+  const X509_NAME *subj_name = NULL;
+  const X509_NAME_ENTRY *cn_entry = NULL;
+  const ASN1_STRING *cn_asn1 = NULL;
   char *cn_str = NULL;
   size_t cn_len = 0;
 
@@ -6180,7 +6180,7 @@ static int ocsp_add_cached_response(pool *p, const char *fingerprint,
   return res;
 }
 
-static int tls_feature_cmp(ASN1_STRING *str, void *feat_data,
+static int tls_feature_cmp(const ASN1_STRING *str, void *feat_data,
     size_t feat_datasz) {
   int is_feat = FALSE, res;
   ASN1_STRING *feat;
@@ -6224,8 +6224,8 @@ static int tls_cert_must_staple(X509 *cert, int *v2) {
 
   for (i = 0; i < ext_count; i++) {
     char buf[1024];
-    X509_EXTENSION *ext;
-    ASN1_OBJECT *obj;
+    const X509_EXTENSION *ext;
+    const ASN1_OBJECT *obj;
 
 #if (OPENSSL_VERSION_NUMBER >= 0x10100000L && !defined(HAVE_LIBRESSL)) || \
     (defined(HAVE_LIBRESSL) && LIBRESSL_VERSION_NUMBER >= 0x3050000L)
@@ -6241,7 +6241,7 @@ static int tls_cert_must_staple(X509 *cert, int *v2) {
     /* Double-check that the OID is that of the "TLS Feature" extension. */
     if (strcmp(buf, TLS_X509V3_TLS_FEAT_OID_TEXT) == 0) {
       char status_request[] = TLS_X509V3_TLS_FEAT_STATUS_REQUEST;
-      ASN1_OCTET_STRING *value;
+      const ASN1_OCTET_STRING *value;
 
 #if (OPENSSL_VERSION_NUMBER >= 0x10100000L && !defined(HAVE_LIBRESSL)) || \
     (defined(HAVE_LIBRESSL) && LIBRESSL_VERSION_NUMBER >= 0x3050000L)
@@ -9073,6 +9073,7 @@ static void tls_end_sess(SSL *ssl, conn_t *conn, int flags) {
   }
 
   if (ssl != ctrl_ssl &&
+      ctrl_ssl != NULL &&
       SSL_get_session(ssl) == SSL_get_session(ctrl_ssl)) {
     /* Uh-oh; our two SSL objects are pointing at the same SSL_SESSION object.
      * This can happen when the SSL_SESSION is resumed, enabled by session
@@ -9095,9 +9096,23 @@ static void tls_end_sess(SSL *ssl, conn_t *conn, int flags) {
      */
     pr_trace_msg(trace_channel, 29,
       "data SSL %p being ended has same SSL_SESSION %p as control SSL, "
-      "clearing the data SSL pointer manually (Issue #1963)", ssl,
+      "clearing the data SSL pointer manually (see Issue #1963)", ssl,
       SSL_get_session(ssl));
-    SSL_set_session(ssl, NULL);
+    if (SSL_set_session(ssl, NULL) != 1) {
+      pr_trace_msg(trace_channel, 29,
+        "error setting NULL session on SSL %p: %s", ssl, tls_get_errors());
+    }
+
+    /* Note that, per findings in Issue #2056, we also need to manually
+     * increment the refcount of the ctrl_ssl session, lest we still
+     * inadvertently corrupt the OpenSSL internal session cache state.
+     * Sigh.
+     */
+    if (SSL_SESSION_up_ref(SSL_get_session(ctrl_ssl)) != 1) {
+      pr_trace_msg(trace_channel, 29,
+        "error incrementing session refcount on SSL %p: %s", ctrl_ssl,
+        tls_get_errors());
+    }
   }
 
   SSL_free(ssl);
@@ -9434,14 +9449,14 @@ static int tls_cert_to_user(const char *user_name, const char *field_name) {
   }
 
   if (strcmp(field_name, "CommonName") == 0) {
-    X509_NAME *name;
+    const X509_NAME *name;
     int pos = -1;
 
     name = X509_get_subject_name(client_cert);
 
     while (TRUE) {
-      X509_NAME_ENTRY *entry;
-      ASN1_STRING *data;
+      const X509_NAME_ENTRY *entry;
+      const ASN1_STRING *data;
       int data_len;
       const unsigned char *data_str = NULL;
 
@@ -9552,8 +9567,8 @@ static int tls_cert_to_user(const char *user_name, const char *field_name) {
       register int i;
 
       for (i = 0; i < nexts; i++) {
-        X509_EXTENSION *ext = NULL;
-        ASN1_OBJECT *asn_object = NULL;
+        const X509_EXTENSION *ext = NULL;
+        const ASN1_OBJECT *asn_object = NULL;
         char oid[PR_TUNABLE_PATH_MAX];
 
         pr_signals_handle();
@@ -9565,7 +9580,7 @@ static int tls_cert_to_user(const char *user_name, const char *field_name) {
         memset(oid, '\0', sizeof(oid));
         if (OBJ_obj2txt(oid, sizeof(oid)-1, asn_object, 1) > 0) {
           if (strcmp(oid, field_name) == 0) {
-            ASN1_OCTET_STRING *asn_data = NULL;
+            const ASN1_OCTET_STRING *asn_data = NULL;
             const unsigned char *asn_datastr = NULL;
             int asn_datalen;
 
@@ -9862,7 +9877,8 @@ static void tls_setup_cert_ext_environ(const char *env_prefix, X509 *cert) {
  *   email                   Email         NID_pkcs9_emailAddress
  */
 
-static void tls_setup_cert_dn_environ(const char *env_prefix, X509_NAME *name) {
+static void tls_setup_cert_dn_environ(const char *env_prefix,
+    const X509_NAME *name) {
   register int i;
   int nentries;
   char *k, *v;
@@ -9874,7 +9890,7 @@ static void tls_setup_cert_dn_environ(const char *env_prefix, X509_NAME *name) {
 #endif /* OpenSSL-1.1.x and later */
 
   for (i = 0; i < nentries; i++) {
-    X509_NAME_ENTRY *entry;
+    const X509_NAME_ENTRY *entry;
     const unsigned char *entry_data;
     int nid, entry_len;
 
@@ -9991,7 +10007,7 @@ static void tls_setup_cert_environ(pool *p, const char *env_prefix,
     char buf[80] = {'\0'};
     ASN1_INTEGER *serial = X509_get_serialNumber(cert);
     const X509_ALGOR *algo = NULL;
-    X509_PUBKEY *pubkey = NULL;
+    const X509_PUBKEY *pubkey = NULL;
 
     memset(buf, '\0', sizeof(buf));
     pr_snprintf(buf, sizeof(buf) - 1, "%lu", X509_get_version(cert) + 1);
@@ -10001,7 +10017,7 @@ static void tls_setup_cert_environ(pool *p, const char *env_prefix,
     v = pstrdup(p, buf);
     pr_env_set(p, k, v);
 
-    if (serial->length < 4) {
+    if (ASN1_STRING_length(serial) < 4) {
       memset(buf, '\0', sizeof(buf));
       pr_snprintf(buf, sizeof(buf) - 1, "%lu", ASN1_INTEGER_get(serial));
       buf[sizeof(buf)-1] = '\0';
@@ -10281,7 +10297,7 @@ static void tls_setup_notes(pool *p, SSL *ssl) {
   client_cert = SSL_get_peer_certificate(ssl);
   if (client_cert != NULL) {
     const X509_ALGOR *algo = NULL;
-    X509_PUBKEY *pubkey = NULL;
+    const X509_PUBKEY *pubkey = NULL;
     BIO *bio = NULL;
     char *data = NULL;
     long datalen = 0;
@@ -10436,8 +10452,8 @@ static int tls_verify_cb(int ok, X509_STORE_CTX *ctx) {
 }
 
 static int tls_verify_crl(int ok, X509_STORE_CTX *ctx) {
-  register int i = 0;
-  X509_NAME *subject = NULL, *issuer = NULL;
+  register unsigned int i = 0;
+  const X509_NAME *subject = NULL, *issuer = NULL;
   X509 *xs = NULL;
   STACK_OF(X509_CRL) *crls = NULL;
   int res, verify_error;
@@ -10615,7 +10631,7 @@ static int tls_verify_ocsp_url(X509_STORE_CTX *ctx, X509 *cert,
     const char *url) {
   BIO *conn;
   X509 *issuing_cert = NULL;
-  X509_NAME *subj = NULL;
+  const X509_NAME *subj = NULL;
   X509_STORE *store = NULL;
   const char *subj_name;
   char *host = NULL, *port = NULL, *uri = NULL;
@@ -11033,8 +11049,14 @@ static int tls_verify_ocsp(int ok, X509_STORE_CTX *ctx) {
         ocsp_urls = make_array(tmp_pool, 1, sizeof(char *));
       }
 
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L && \
+    !defined(HAVE_LIBRESSL)
       *((char **) push_array(ocsp_urls)) = pstrdup(tmp_pool,
-        (char *) desc->location->d.uniformResourceIdentifier->data);
+        (char *) ASN1_STRING_get0_data(desc->location->d.uniformResourceIdentifier));
+#else
+      *((char **) push_array(ocsp_urls)) = pstrdup(tmp_pool,
+        (char *) ASN1_STRING_data(desc->location->d.uniformResourceIdentifier));
+#endif /* OpenSSL 1.1.x and later */
     }
   }
 
@@ -11102,7 +11124,7 @@ static ssize_t tls_write(SSL *ssl, const void *buf, size_t len) {
   return count;
 }
 
-static char *tls_x509_name_oneline(X509_NAME *x509_name) {
+static char *tls_x509_name_oneline(const X509_NAME *x509_name) {
   static char buf[1024] = {'\0'};
 
   /* If we are using OpenSSL 0.9.6 or newer, we want to use


=====================================
contrib/mod_wrap2_sql.c
=====================================
@@ -1,7 +1,7 @@
 /*
  * ProFTPD: mod_wrap2_sql -- a mod_wrap2 sub-module for supplying IP-based
  *                           access control data via SQL tables
- * Copyright (c) 2002-2016 TJ Saunders
+ * Copyright (c) 2002-2026 TJ Saunders
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -25,7 +25,7 @@
 #include "mod_wrap2.h"
 #include "mod_sql.h"
 
-#define MOD_WRAP2_SQL_VERSION		"mod_wrap2_sql/1.0"
+#define MOD_WRAP2_SQL_VERSION		"mod_wrap2_sql/1.1"
 
 #define WRAP2_SQL_NSLOTS		2
 #define WRAP2_SQL_CLIENT_QUERY_IDX	0
@@ -62,6 +62,41 @@ static int sqltab_close_cb(wrap2_table_t *sqltab) {
   return 0;
 }
 
+static char *sqltab_get_escaped_text(pool *p, wrap2_table_t *sqltab,
+    const char *text) {
+  pool *tmp_pool = NULL;
+  cmdtable *sql_cmdtab = NULL;
+  cmd_rec *sql_cmd = NULL;
+  modret_t *sql_res = NULL;
+
+  /* Find the cmdtable for the sql_escapestr command, as the provided
+   * name needs to be properly escaped for SQL syntax; see Issue #2057.
+   */
+  sql_cmdtab = pr_stash_get_symbol2(PR_SYM_HOOK, "sql_escapestr", NULL, NULL,
+    NULL);
+  if (sql_cmdtab == NULL) {
+    wrap2_log("error: unable to find SQL hook symbol 'sql_escapestr': "
+      "perhaps your proftpd.conf needs 'LoadModule mod_sql.c'?");
+    return NULL;
+  }
+
+  sql_cmd = sql_cmd_create(tmp_pool, 1, text);
+  sql_res = pr_module_call(sql_cmdtab->m, sql_cmdtab->handler, sql_cmd);
+  if (sql_res == NULL) {
+    wrap2_log("sql_escapestr '%s' returned no data; "
+      "see the mod_sql.c SQLLogFile for more details", text);
+    return NULL;
+  }
+
+  if (MODRET_ISERROR(sql_res)) {
+    wrap2_log("error processing sql_escapestr '%s': "
+      "check the mod_sql.c SQLLogFile for more details", text);
+    return NULL;
+  }
+
+  return sql_res->data;
+}
+
 static array_header *sqltab_fetch_clients_cb(wrap2_table_t *sqltab,
     const char *name) {
   register unsigned int i;
@@ -70,12 +105,18 @@ static array_header *sqltab_fetch_clients_cb(wrap2_table_t *sqltab,
   cmd_rec *sql_cmd = NULL;
   modret_t *sql_res = NULL;
   array_header *sql_data = NULL;
-  char *query = NULL, **vals = NULL;
+  char *escaped_name = NULL, *query = NULL, **vals = NULL;
   array_header *clients_list = NULL;
 
   /* Allocate a temporary pool for the duration of this read. */
   tmp_pool = make_sub_pool(sqltab->tab_pool);
 
+  escaped_name = sqltab_get_escaped_text(tmp_pool, sqltab, name);
+  if (escaped_name == NULL) {
+    destroy_pool(tmp_pool);
+    return NULL;
+  }
+
   query = ((char **) sqltab->tab_data)[WRAP2_SQL_CLIENT_QUERY_IDX];
 
   /* Find the cmdtable for the sql_lookup command. */
@@ -89,7 +130,7 @@ static array_header *sqltab_fetch_clients_cb(wrap2_table_t *sqltab,
   }
 
   /* Prepare the SELECT query. */
-  sql_cmd = sql_cmd_create(tmp_pool, 3, "sql_lookup", query, name);
+  sql_cmd = sql_cmd_create(tmp_pool, 3, "sql_lookup", query, escaped_name);
 
   /* Call the handler. */
   sql_res = pr_module_call(sql_cmdtab->m, sql_cmdtab->handler, sql_cmd);
@@ -192,16 +233,22 @@ static array_header *sqltab_fetch_options_cb(wrap2_table_t *sqltab,
   cmd_rec *sql_cmd = NULL;
   modret_t *sql_res = NULL;
   array_header *sql_data = NULL;
-  char *query = NULL, **vals = NULL;
+  char *escaped_name = NULL, *query = NULL, **vals = NULL;
   array_header *options_list = NULL;
 
   /* Allocate a temporary pool for the duration of this read. */
   tmp_pool = make_sub_pool(sqltab->tab_pool);
 
+  escaped_name = sqltab_get_escaped_text(tmp_pool, sqltab, name);
+  if (escaped_name == NULL) {
+    destroy_pool(tmp_pool);
+    return NULL;
+  }
+
   query = ((char **) sqltab->tab_data)[WRAP2_SQL_OPTION_QUERY_IDX];
 
   /* The options-query is not necessary.  Skip if not present. */
-  if (!query) {
+  if (query == NULL) {
     destroy_pool(tmp_pool);
     return NULL;
   }
@@ -217,7 +264,7 @@ static array_header *sqltab_fetch_options_cb(wrap2_table_t *sqltab,
   }
 
   /* Prepare the SELECT query. */
-  sql_cmd = sql_cmd_create(tmp_pool, 3, "sql_lookup", query, name);
+  sql_cmd = sql_cmd_create(tmp_pool, 3, "sql_lookup", query, escaped_name);
 
   /* Call the handler. */
   sql_res = pr_module_call(sql_cmdtab->m, sql_cmdtab->handler, sql_cmd);


=====================================
include/version.h
=====================================
@@ -28,8 +28,8 @@
 #include "buildstamp.h"
 
 /* Application version (in various forms) */
-#define PROFTPD_VERSION_NUMBER		0x0001030905
-#define PROFTPD_VERSION_TEXT		"1.3.9a"
+#define PROFTPD_VERSION_NUMBER		0x0001030906
+#define PROFTPD_VERSION_TEXT		"1.3.9b"
 
 /* Module API version */
 #define PR_MODULE_API_VERSION		0x20


=====================================
tests/t/lib/ProFTPD/Tests/Modules/mod_sftp/quotatab.pm
=====================================
@@ -0,0 +1,271 @@
+package ProFTPD::Tests::Modules::mod_sftp::quotatab;
+
+use lib qw(t/lib);
+use base qw(ProFTPD::TestSuite::Child);
+use strict;
+
+use File::Path qw(mkpath);
+use File::Spec;
+use IO::Handle;
+use POSIX qw(:fcntl_h);
+
+use ProFTPD::TestSuite::FTP;
+use ProFTPD::TestSuite::Utils qw(:auth :config :running :test :testsuite);
+
+$| = 1;
+
+my $order = 0;
+
+my $TESTS = {
+  sftp_quotatab_upload_hard_limit_issue2098 => {
+    order => ++$order,
+    test_class => [qw(bug forking mod_quotatab_sql mod_sql_sqlite)],
+  },
+
+};
+
+sub new {
+  return shift()->SUPER::new(@_);
+}
+
+sub list_tests {
+  return testsuite_get_runnable_tests($TESTS);
+}
+
+sub set_up {
+  my $self = shift;
+  $self->SUPER::set_up(@_);
+
+  # Make sure that mod_sftp does not complain about permissions on the hostkey
+  # files.
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  unless (chmod(0400, $rsa_host_key, $dsa_host_key)) {
+    die("Can't set perms on $rsa_host_key, $dsa_host_key: $!");
+  }
+}
+
+sub sftp_quotatab_upload_hard_limit_issue2098 {
+  my $self = shift;
+  my $tmpdir = $self->{tmpdir};
+  my $setup = test_setup($tmpdir, 'quotatab');
+
+  my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_rsa_key');
+  my $dsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp/ssh_host_dsa_key');
+
+  my $db_file = File::Spec->rel2abs("$tmpdir/proftpd.db");
+
+  my $db_script = File::Spec->rel2abs("$tmpdir/proftpd.sql");
+
+  if (open(my $fh, "> $db_script")) {
+    print $fh <<EOS;
+CREATE TABLE quotalimits (
+  name TEXT NOT NULL PRIMARY KEY,
+  quota_type TEXT NOT NULL,
+  per_session TEXT NOT NULL,
+  limit_type TEXT NOT NULL,
+  bytes_in_avail REAL NOT NULL,
+  bytes_out_avail REAL NOT NULL,
+  bytes_xfer_avail REAL NOT NULL,
+  files_in_avail INTEGER NOT NULL,
+  files_out_avail INTEGER NOT NULL,
+  files_xfer_avail INTEGER NOT NULL
+);
+INSERT INTO quotalimits (name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail) VALUES ('$setup->{user}', 'user', 'false', 'hard', 32, 0, 0, 2, 0, 0);
+
+CREATE TABLE quotatallies (
+  name TEXT NOT NULL PRIMARY KEY,
+  quota_type TEXT NOT NULL,
+  bytes_in_used REAL NOT NULL,
+  bytes_out_used REAL NOT NULL,
+  bytes_xfer_used REAL NOT NULL,
+  files_in_used INTEGER NOT NULL,
+  files_out_used INTEGER NOT NULL,
+  files_xfer_used INTEGER NOT NULL
+);
+EOS
+
+    unless (close($fh)) {
+      die("Can't write $db_script: $!");
+    }
+
+  } else {
+    die("Can't open $db_script: $!");
+  }
+
+  my $cmd = "sqlite3 $db_file < $db_script";
+
+  if ($ENV{TEST_VERBOSE}) {
+    print STDERR "Executing sqlite3: $cmd\n";
+  }
+
+  my @output = `$cmd`;
+  if (scalar(@output) &&
+      $ENV{TEST_VERBOSE}) {
+    print STDERR "Output: ", join('', @output), "\n";
+  }
+
+  # Make sure that, if we're running as root, the database file has
+  # the permissions/privs set for use by proftpd
+  if ($< == 0) {
+    unless (chmod(0666, $db_file)) {
+      die("Can't set perms on $db_file to 0666: $!");
+    }
+  }
+
+  my $config = {
+    PidFile => $setup->{pid_file},
+    ScoreboardFile => $setup->{scoreboard_file},
+    SystemLog => $setup->{log_file},
+    TraceLog => $setup->{log_file},
+    Trace => 'fsio:20 quotatab:20 sql:20 ssh2:20 sftp:30',
+
+    AuthUserFile => $setup->{auth_user_file},
+    AuthGroupFile => $setup->{auth_group_file},
+    AuthOrder => 'mod_auth_file.c',
+
+    IfModules => {
+      'mod_delay.c' => {
+        DelayEngine => 'off',
+      },
+
+      'mod_quotatab_sql.c' => [
+        'SQLNamedQuery get-quota-limit SELECT "name, quota_type, per_session, limit_type, bytes_in_avail, bytes_out_avail, bytes_xfer_avail, files_in_avail, files_out_avail, files_xfer_avail FROM quotalimits WHERE name = \'%{0}\' AND quota_type = \'%{1}\'"',
+        'SQLNamedQuery get-quota-tally SELECT "name, quota_type, bytes_in_used, bytes_out_used, bytes_xfer_used, files_in_used, files_out_used, files_xfer_used FROM quotatallies WHERE name = \'%{0}\' AND quota_type = \'%{1}\'"',
+        'SQLNamedQuery update-quota-tally UPDATE "bytes_in_used = bytes_in_used + %{0}, bytes_out_used = bytes_out_used + %{1}, bytes_xfer_used = bytes_xfer_used + %{2}, files_in_used = files_in_used + %{3}, files_out_used = files_out_used + %{4}, files_xfer_used = files_xfer_used + %{5} WHERE name = \'%{6}\' AND quota_type = \'%{7}\'" quotatallies',
+        'SQLNamedQuery insert-quota-tally INSERT "%{0}, %{1}, %{2}, %{3}, %{4}, %{5}, %{6}, %{7}" quotatallies',
+
+        'QuotaEngine on',
+        "QuotaLog $setup->{log_file}",
+        'QuotaLimitTable sql:/get-quota-limit',
+        'QuotaTallyTable sql:/get-quota-tally/update-quota-tally/insert-quota-tally',
+      ],
+
+      'mod_sftp.c' => [
+        "SFTPEngine on",
+        "SFTPLog $setup->{log_file}",
+        "SFTPHostKey $rsa_host_key",
+        "SFTPHostKey $dsa_host_key",
+      ],
+
+      'mod_sql.c' => {
+        SQLEngine => 'log',
+        SQLBackend => 'sqlite3',
+        SQLConnectInfo => $db_file,
+        SQLLogFile => $setup->{log_file},
+      },
+    },
+  };
+
+  my ($port, $config_user, $config_group) = config_write($setup->{config_file},
+    $config);
+
+  # Open pipes, for use between the parent and child processes.  Specifically,
+  # the child will indicate when it's done with its test by writing a message
+  # to the parent.
+  my ($rfh, $wfh);
+  unless (pipe($rfh, $wfh)) {
+    die("Can't open pipe: $!");
+  }
+
+  require Net::SSH2;
+
+  my $ex;
+
+  # Fork child
+  $self->handle_sigchld();
+  defined(my $pid = fork()) or die("Can't fork: $!");
+  if ($pid) {
+    eval {
+      my $ssh2 = Net::SSH2->new();
+
+      sleep(1);
+
+      unless ($ssh2->connect('127.0.0.1', $port)) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      unless ($ssh2->auth_password($setup->{user}, $setup->{passwd})) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't login to SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $sftp = $ssh2->sftp();
+      unless ($sftp) {
+        my ($err_code, $err_name, $err_str) = $ssh2->error();
+        die("Can't use SFTP on SSH2 server: [$err_name] ($err_code) $err_str");
+      }
+
+      my $fh = $sftp->open('test.dat', O_WRONLY|O_CREAT|O_TRUNC, 0644);
+      unless ($fh) {
+        my ($err_code, $err_name) = $sftp->error();
+        die("Can't open test.txt: [$err_name] ($err_code)");
+      }
+
+      my $count = 20;
+      for (my $i = 0; $i < $count; $i++) {
+        print $fh "ABCD" x 8192;
+      }
+
+      # To issue the FXP_CLOSE, we have to explicitly destroy the filehandle
+      $fh = undef;
+
+      $sftp = undef;
+      $ssh2->disconnect();
+    };
+    if ($@) {
+      $ex = $@;
+    }
+
+    $wfh->print("done\n");
+    $wfh->flush();
+
+  } else {
+    eval { server_wait($setup->{config_file}, $rfh) };
+    if ($@) {
+      warn($@);
+      exit 1;
+    }
+
+    exit 0;
+  }
+
+  # Stop server
+  server_stop($setup->{pid_file});
+  $self->assert_child_ok($pid);
+
+  eval {
+    if (open(my $fh, "< $setup->{log_file}")) {
+      my $ok = 0;
+
+      while (my $line = <$fh>) {
+        chomp($line);
+
+        if ($ENV{TEST_VERBOSE}) {
+          print STDERR "# $line\n";
+        }
+
+        if ($line =~ /limit exceeded/) {
+          $ok = 1;
+          last;
+        }
+      }
+
+      close($fh);
+      $self->assert($ok, test_msg("Did not see expected QuotaLog message"));
+
+    } else {
+      die("Can't read $setup->{log_file}: $!");
+    }
+  };
+  if ($@) {
+    $ex = $@;
+  }
+
+  test_cleanup($setup->{log_file}, $ex);
+}
+
+1;


=====================================
tests/t/modules/mod_sftp/quotatab.t
=====================================
@@ -0,0 +1,11 @@
+#!/usr/bin/env perl
+
+use lib qw(t/lib);
+use strict;
+
+use Test::Unit::HarnessUnit;
+
+$| = 1;
+
+my $r = Test::Unit::HarnessUnit->new();
+$r->start("ProFTPD::Tests::Modules::mod_sftp::quotatab");



View it on GitLab: https://salsa.debian.org/debian-proftpd-team/proftpd/-/commit/6d6fe6ac47162620d9d09a3d12012a81b080bab5

-- 
View it on GitLab: https://salsa.debian.org/debian-proftpd-team/proftpd/-/commit/6d6fe6ac47162620d9d09a3d12012a81b080bab5
You're receiving this email because of your account on salsa.debian.org. Manage all notifications: https://salsa.debian.org/-/profile/notifications | Help: https://salsa.debian.org/help




More information about the Pkg-proftpd-maintainers mailing list