From 415395b795436ae47cc25b2394e80033b80f11be Mon Sep 17 00:00:00 2001 From: TJ Saunders 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 --- 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);