package/proftpd: patch CVE-2026-42167

- CVE-2026-42167:
    mod_sql in ProFTPD before 1.3.9a allows remote attackers to execute
    arbitrary code via a username, in scenarios where there is logging of
    USER requests with an expansion such as %U, and the SQL backend allows
    commands (e.g., COPY TO PROGRAM).

For more information, see:
 - https://www.cve.org/CVERecord?id=CVE-2026-42167
 - https://github.com/proftpd/proftpd/issues/2052
 - af90843baf

The backport was provided by the Fedora Community:
 - https://src.fedoraproject.org/rpms/proftpd/raw/epel9/f/2052.patch

(cherry picked from commit 0d5ce9ed84)
Signed-off-by: Thomas Perale <thomas.perale@mind.be>
This commit is contained in:
Thomas Perale
2026-05-13 12:39:07 +02:00
parent 802d147f37
commit 796bf397a4
2 changed files with 202 additions and 0 deletions

View File

@@ -0,0 +1,199 @@
From 415395b795436ae47cc25b2394e80033b80f11be Mon Sep 17 00:00:00 2001
From: TJ Saunders <tj@castaglia.org>
Date: Mon, 27 Apr 2026 12:13:09 -0700
Subject: [PATCH] Issue #2052: When resolving any variable whose value is
supplied by the client, make sure we **always** escape that value text.
CVE: CVE-2026-42167
Upstream: https://github.com/proftpd/proftpd/commit/af90843baf7dcb8c6be1e5261be2d0b5b5850673
Upstream: https://src.fedoraproject.org/rpms/proftpd/raw/epel9/f/2052.patch
Signed-off-by: Thomas Perale <thomas.perale@mind.be>
---
contrib/mod_sql.c | 103 ++++++++++++++++++++++++++++------------------
1 file changed, 64 insertions(+), 39 deletions(-)
diff --git a/contrib/mod_sql.c b/contrib/mod_sql.c
index 4978b38b70..4872f5fa64 100644
--- a/contrib/mod_sql.c
+++ b/contrib/mod_sql.c
@@ -2,7 +2,7 @@
* ProFTPD: mod_sql -- SQL frontend
* Copyright (c) 1998-1999 Johnie Ingram.
* Copyright (c) 2001 Andrew Houghton.
- * Copyright (c) 2004-2022 TJ Saunders
+ * Copyright (c) 2004-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
@@ -760,40 +760,46 @@ static int is_escaped_text(const char *text, size_t text_len) {
}
static int sql_resolved_append_text(pool *p, struct sql_resolved *resolved,
- const char *text, size_t text_len) {
- char *new_text;
- size_t new_textlen;
+ const char *text, size_t text_len, int already_escaped) {
+ char *new_text = NULL;
+ size_t new_textlen = 0;
if (text == NULL ||
text_len == 0) {
return 0;
}
- /* For backward compatibility (see Issue #1149), we indulge in a little
- * heuristic here, and only escape the text if it hasn't already been
- * escaped. How to properly tell? If the first and last characters of
- * the given text are `'`, AND there are no other occurrences of that
- * character in the text, assume it has already been quoted.
- */
- if (is_escaped_text(text, text_len) == FALSE) {
- modret_t *mr;
+ new_text = (char *) text;
+ new_textlen = text_len;
- mr = sql_dispatch(sql_make_cmd(p, 2, resolved->conn_name, text),
- "sql_escapestring");
- if (check_response(mr, resolved->conn_flags) < 0) {
- errno = EIO;
- return -1;
- }
+ if (already_escaped == FALSE) {
+ /* For backward compatibility (see Issue #1149), we indulge in a little
+ * heuristic here, and only escape the text if it hasn't already been
+ * escaped. How to properly tell? If the first and last characters of
+ * the given text are `'`, AND there are no other occurrences of that
+ * character in the text, assume it has already been quoted.
+ *
+ * Per Issue #2052, we refine this to use this heuristic only if we do
+ * not already know that the text has been escaped. Some callers may
+ * have already escaped the provided text for us.
+ */
+ if (is_escaped_text(text, text_len) == FALSE) {
+ modret_t *mr;
- new_text = (char *) mr->data;
- new_textlen = strlen(new_text);
+ mr = sql_dispatch(sql_make_cmd(p, 2, resolved->conn_name, text),
+ "sql_escapestring");
+ if (check_response(mr, resolved->conn_flags) < 0) {
+ errno = EIO;
+ return -1;
+ }
- } else {
- pr_trace_msg(trace_channel, 17,
- "text '%s' is already escaped, skipping escaping it again", text);
+ new_text = (char *) mr->data;
+ new_textlen = strlen(new_text);
- new_text = (char *) text;
- new_textlen = text_len;
+ } else {
+ pr_trace_msg(trace_channel, 17,
+ "text '%s' is already escaped, skipping escaping it again", text);
+ }
}
if (new_textlen > resolved->buflen) {
@@ -811,7 +817,7 @@ static int sql_resolved_append_text(pool *p, struct sql_resolved *resolved,
static int sql_resolve_on_meta(pool *p, pr_jot_ctx_t *jot_ctx,
unsigned char logfmt_id, const char *jot_hint, const void *val) {
- int res = 0;
+ int res = 0, already_escaped = FALSE;
struct sql_resolved *resolved;
resolved = jot_ctx->log;
@@ -970,35 +976,53 @@ static int sql_resolve_on_meta(pool *p, pr_jot_ctx_t *jot_ctx,
break;
}
+ /* Per Issue #2052, the following variable values can all be supplied
+ * remotely by the client. As such, they should be escaped preemptively.
+ */
case LOGFMT_META_ANON_PASS:
case LOGFMT_META_BASENAME:
- case LOGFMT_META_CLASS:
case LOGFMT_META_CMD_PARAMS:
case LOGFMT_META_COMMAND:
case LOGFMT_META_DIR_NAME:
case LOGFMT_META_DIR_PATH:
+ case LOGFMT_META_FILENAME:
+ case LOGFMT_META_IDENT_USER:
+ case LOGFMT_META_METHOD:
+ case LOGFMT_META_ORIGINAL_USER:
+ case LOGFMT_META_RESPONSE_STR:
+ case LOGFMT_META_REMOTE_HOST:
+ case LOGFMT_META_RENAME_FROM:
+ case LOGFMT_META_USER:
+ case LOGFMT_META_XFER_PATH: {
+ modret_t *mr;
+
+ mr = sql_dispatch(sql_make_cmd(p, 2, resolved->conn_name,
+ (const char *) val), "sql_escapestring");
+ if (check_response(mr, resolved->conn_flags) < 0) {
+ errno = EIO;
+ return -1;
+ }
+
+ text = (char *) mr->data;
+ text_len = strlen(text);
+ already_escaped = TRUE;
+ break;
+ }
+
+ case LOGFMT_META_CLASS:
case LOGFMT_META_ENV_VAR:
case LOGFMT_META_EOS_REASON:
- case LOGFMT_META_FILENAME:
case LOGFMT_META_GROUP:
- case LOGFMT_META_IDENT_USER:
case LOGFMT_META_ISO8601:
case LOGFMT_META_LOCAL_FQDN:
case LOGFMT_META_LOCAL_IP:
case LOGFMT_META_LOCAL_NAME:
- case LOGFMT_META_METHOD:
case LOGFMT_META_NOTE_VAR:
- case LOGFMT_META_ORIGINAL_USER:
case LOGFMT_META_PROTOCOL:
- case LOGFMT_META_REMOTE_HOST:
case LOGFMT_META_REMOTE_IP:
- case LOGFMT_META_RENAME_FROM:
- case LOGFMT_META_RESPONSE_STR:
- case LOGFMT_META_USER:
case LOGFMT_META_VERSION:
case LOGFMT_META_VHOST_IP:
case LOGFMT_META_XFER_FAILURE:
- case LOGFMT_META_XFER_PATH:
case LOGFMT_META_XFER_STATUS:
case LOGFMT_META_XFER_TYPE:
default:
@@ -1011,7 +1035,8 @@ static int sql_resolve_on_meta(pool *p, pr_jot_ctx_t *jot_ctx,
text_len = strlen(text);
}
- res = sql_resolved_append_text(p, resolved, text, text_len);
+ res = sql_resolved_append_text(p, resolved, text, text_len,
+ already_escaped);
}
return res;
@@ -1074,7 +1099,7 @@ static int sql_resolve_on_default(pool *p, pr_jot_ctx_t *jot_ctx,
break;
}
- res = sql_resolved_append_text(p, resolved, text, text_len);
+ res = sql_resolved_append_text(p, resolved, text, text_len, FALSE);
}
return res;
@@ -3175,7 +3200,7 @@ static int showinfo_on_meta(pool *p, pr_jot_ctx_t *jot_ctx,
}
text_len = strlen(text);
- res = sql_resolved_append_text(p, resolved, text, text_len);
+ res = sql_resolved_append_text(p, resolved, text, text_len, FALSE);
} else {
res = sql_resolve_on_meta(p, jot_ctx, logfmt_id, jot_hint, val);

View File

@@ -11,6 +11,9 @@ PROFTPD_LICENSE_FILES = COPYING
PROFTPD_CPE_ID_VENDOR = proftpd
PROFTPD_SELINUX_MODULES = ftp
# 0001-CVE-2026-42167.patch
PROFTPD_IGNORE_CVES += CVE-2026-42167
PROFTPD_CONF_ENV = \
ac_cv_func_setpgrp_void=yes \
ac_cv_func_setgrent_void=yes