Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package vsftpd for openSUSE:Factory checked in at 2026-11-10 16:29:44 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/vsftpd (Old) and /work/SRC/openSUSE:Factory/.vsftpd.new.3759290 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "vsftpd" Tue Nov 10 16:29:44 2026 rev:96 rq:1396499 version:3.0.5 Changes: -------- --- /work/SRC/openSUSE:Factory/vsftpd/vsftpd.changes 2026-11-08 19:21:45.592320276 +0100 +++ /work/SRC/openSUSE:Factory/.vsftpd.new.3759290/vsftpd.changes 2026-11-10 16:29:53.549468034 +0100 @@ -1,0 +2,31 @@ +Tue Nov 10 11:43:32 UTC 2026 - Peter Simons <[email protected]> + +- Add vsftpd-CVE-2026-119567.patch: make cmds_allowed and cmds_denied + apply to the RFC 775 aliases XMKD, XRMD, XPWD, XCWD and XCUP as + well. Previously authenticated users could run denied commands by + issuing their X-prefixed alias, e.g. XRMD instead of RMD. + [CVE-2026-119567, bsc#1288995] + +------------------------------------------------------------------- +Tue Nov 10 11:33:17 UTC 2026 - Peter Simons <[email protected]> + +- Add vsftpd-no-stderr-on-control-connection.patch: session processes + no longer have the client connection on stderr. Diagnostics printed + by library code, such as "yp_bind_client_create_v3: RPC: Unable to + send" from NIS lookups, were sent to FTP clients verbatim. They now + go to the listener's stderr (the journal under systemd) or + /dev/null instead. [bsc#1184651] + +- [email protected]: set StandardError=journal so that such messages are + logged when vsftpd is socket-activated. + +------------------------------------------------------------------- +Mon Nov 9 15:39:43 UTC 2026 - Peter Simons <[email protected]> + +- Add vsftpd-CVE-2026-119568.patch: bind TLS sessions resumed on + the data channel to the session of the corresponding control + connection. Previously require_ssl_reuse accepted any resumed + session ticket, allowing an attacker sharing the victim's source + IP to hijack FTPS data connections. [CVE-2026-119568, bsc#1288996] + +------------------------------------------------------------------- New: ---- vsftpd-CVE-2026-119567.patch vsftpd-CVE-2026-119568.patch vsftpd-no-stderr-on-control-connection.patch ----------(New B)---------- New: - Add vsftpd-CVE-2026-119567.patch: make cmds_allowed and cmds_denied apply to the RFC 775 aliases XMKD, XRMD, XPWD, XCWD and XCUP as New: - Add vsftpd-CVE-2026-119568.patch: bind TLS sessions resumed on the data channel to the session of the corresponding control New: - Add vsftpd-no-stderr-on-control-connection.patch: session processes no longer have the client connection on stderr. Diagnostics printed ----------(New E)---------- ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ vsftpd.spec ++++++ --- /var/tmp/diff_new_pack.p8BBoE/_old 2026-11-10 16:29:55.059531197 +0100 +++ /var/tmp/diff_new_pack.p8BBoE/_new 2026-11-10 16:29:55.061531280 +0100 @@ -102,6 +102,12 @@ #PATCH-FIX-OPENSUSE bsc#1211301 Enable crypto-policies support Patch47: vsftpd-use-system-wide-crypto-policy.patch Patch48: terminate-peers-on-quit.patch +#PATCH-FIX-OPENSUSE bsc#1288996 CVE-2026-119568 bind data channel TLS session to control channel +Patch49: vsftpd-CVE-2026-119568.patch +#PATCH-FIX-OPENSUSE do not leak library diagnostics on stderr to FTP clients +Patch50: vsftpd-no-stderr-on-control-connection.patch +#PATCH-FIX-OPENSUSE bsc#1288995 CVE-2026-119567 apply cmds_denied to RFC 775 aliases +Patch51: vsftpd-CVE-2026-119567.patch BuildRequires: libcap-devel %if 0%{?suse_version} == 1315 BuildRequires: libopenssl-1_1-devel >= 1.1.1 @@ -191,6 +197,9 @@ %patch -P 46 -p1 %patch -P 47 -p1 %patch -P 48 -p1 +%patch -P 49 -p1 +%patch -P 50 -p1 +%patch -P 51 -p1 %build %define seccomp_opts -D_GNU_SOURCE -DUSE_SECCOMP ++++++ vsftpd-CVE-2026-119567.patch ++++++ From: Peter Simons <[email protected]> Subject: Apply cmds_allowed / cmds_denied to RFC 775 command aliases process_post_login() compared the literal command token against the cmds_allowed and cmds_denied lists, but dispatched the RFC 775 aliases XMKD, XRMD, XPWD, XCWD and XCUP to the same handlers as MKD, RMD, PWD, CWD and CDUP. An authenticated user could therefore bypass e.g. cmds_denied=RMD by sending XRMD instead. Both the received command and the configured list entries are now mapped to their canonical name before comparison. List entries are also upper-cased, because commands received from the client already are, so lower-case entries previously never matched anything. CVE-2026-119567, bsc#1288995 --- a/postlogin.c +++ b/postlogin.c @@ -80,6 +80,36 @@ static void get_unique_filename(struct m static int data_transfer_checks_ok(struct vsf_session* p_sess); static void resolve_tilde(struct mystr* p_str, struct vsf_session* p_sess); +static void +canonicalize_cmd(struct mystr* p_cmd_str) +{ + /* Map the RFC 775 X-prefixed aliases to the commands they are equivalent + * to, so that cmds_allowed / cmds_denied cannot be bypassed by using the + * alias of a denied command. + */ + str_upper(p_cmd_str); + if (str_equal_text(p_cmd_str, "XMKD")) + { + str_alloc_text(p_cmd_str, "MKD"); + } + else if (str_equal_text(p_cmd_str, "XRMD")) + { + str_alloc_text(p_cmd_str, "RMD"); + } + else if (str_equal_text(p_cmd_str, "XPWD")) + { + str_alloc_text(p_cmd_str, "PWD"); + } + else if (str_equal_text(p_cmd_str, "XCWD")) + { + str_alloc_text(p_cmd_str, "CWD"); + } + else if (str_equal_text(p_cmd_str, "XCUP")) + { + str_alloc_text(p_cmd_str, "CDUP"); + } +} + void process_post_login(struct vsf_session* p_sess) { @@ -115,6 +145,7 @@ process_post_login(struct vsf_session* p while(1) { + static struct mystr s_canon_cmd_str; int cmd_ok = 1; if (tunable_setproctitle_enable) { @@ -138,6 +169,11 @@ process_post_login(struct vsf_session* p str_free(&proctitle_str); } /* Test command against the allowed lists.. */ + if (tunable_cmds_allowed || tunable_cmds_denied) + { + str_copy(&s_canon_cmd_str, &p_sess->ftp_cmd_str); + canonicalize_cmd(&s_canon_cmd_str); + } if (tunable_cmds_allowed) { static struct mystr s_src_str; @@ -151,7 +187,8 @@ process_post_login(struct vsf_session* p cmd_ok = 0; break; } - else if (str_equal(&s_src_str, &p_sess->ftp_cmd_str)) + canonicalize_cmd(&s_src_str); + if (str_equal(&s_src_str, &s_canon_cmd_str)) { break; } @@ -170,7 +207,8 @@ process_post_login(struct vsf_session* p { break; } - else if (str_equal(&s_src_str, &p_sess->ftp_cmd_str)) + canonicalize_cmd(&s_src_str); + if (str_equal(&s_src_str, &s_canon_cmd_str)) { cmd_ok = 0; break; --- a/vsftpd.conf.5 +++ b/vsftpd.conf.5 @@ -864,6 +864,11 @@ login. USER, PASS, QUIT and others are a appears on both this and .BR cmds_allowed then the denial takes precedence. (Added in v2.1.0). +The RFC 775 aliases XCUP, XCWD, XMKD, XPWD and XRMD are treated like CDUP, +CWD, MKD, PWD and RMD respectively by both +.BR cmds_allowed +and +.BR cmds_denied . Default: (none) .TP ++++++ vsftpd-CVE-2026-119568.patch ++++++ From: Peter Simons <[email protected]> Subject: Bind data channel TLS sessions to the control channel session require_ssl_reuse only checked SSL_session_reused() on the data channel. In standalone mode all session processes inherit the same SSL_CTX and therefore the same session ticket key, so a ticket an attacker obtained via AUTH TLS on their own control connection was accepted on the PASV data port of another user's session (from the same source IP), allowing download theft or upload injection. Each session process now generates a random token before its control handshake and stores it as ticket appdata in all sessions issued on the control channel. Foreign tickets are ignored on decryption, and data channel sessions are only accepted if they carry this process' token. CVE-2026-119568, bsc#1288996 --- a/ssl.c +++ b/ssl.c @@ -55,9 +55,30 @@ static int ssl_read_common(struct vsf_se unsigned int len, int (*p_ssl_func)(SSL*, void*, int)); +static int ssl_ticket_gen_callback(SSL* p_ssl, void* p_arg); +static SSL_TICKET_RETURN ssl_ticket_dec_callback(SSL* p_ssl, + SSL_SESSION* p_ssl_sess, + const unsigned char* p_keyname, + size_t keyname_len, + SSL_TICKET_STATUS status, + void* p_arg); +static int ssl_session_is_bound(const SSL_SESSION* p_ssl_sess); + static int ssl_inited; static struct mystr debug_str; +/* SECURITY: random token generated by each session process right before its + * control channel TLS handshake. It is stored as ticket appdata in every TLS + * session issued on that control channel. Data channel sessions are only + * accepted if they resume a session carrying this token. In standalone mode + * all session processes share one SSL_CTX (and thus one ticket key), so + * merely checking SSL_session_reused() would accept a ticket obtained by an + * attacker on a completely different control connection (CVE-2026-119568). + */ +#define VSF_SSL_BIND_TOKEN_LEN 32 +static unsigned char s_bind_token[VSF_SSL_BIND_TOKEN_LEN]; +static int s_bind_token_set; + void ssl_init(struct vsf_session* p_sess) { @@ -184,6 +205,12 @@ ssl_init(struct vsf_session* p_sess) /* Ensure cached session doesn't expire */ SSL_CTX_set_timeout(p_ctx, INT_MAX); } + /* Bind session tickets to the control connection they were issued on. */ + if (!SSL_CTX_set_session_ticket_cb(p_ctx, ssl_ticket_gen_callback, + ssl_ticket_dec_callback, NULL)) + { + die("SSL: could not set session ticket callbacks"); + } /* Set up ALPN to check for FTP protocol intention of client. */ SSL_CTX_set_alpn_select_cb(p_ctx, ssl_alpn_callback, p_sess); /* Set up SNI callback for an optional hostname check. */ @@ -499,6 +526,15 @@ ssl_accept(struct vsf_session* p_sess, i ssl_data_close(p_sess); return 0; } + if (tunable_require_ssl_reuse && + !ssl_session_is_bound(SSL_get0_session(p_ssl))) + { + str_alloc_text(&debug_str, + "SSL session on data channel not from control channel."); + vsf_log_line(p_sess, kVSFLogEntryDebug, &debug_str); + ssl_data_close(p_sess); + return 0; + } if (str_getlen(&p_sess->control_cert_digest) > 0) { static struct mystr data_cert_digest; @@ -637,11 +673,32 @@ get_ssl(struct vsf_session* p_sess, int static int ssl_session_init(struct vsf_session* p_sess) { - SSL* p_ssl = get_ssl(p_sess, VSFTP_COMMAND_FD); + SSL* p_ssl; + /* Must happen in the per-session process, before the control handshake + * issues any session tickets. + */ + if (!s_bind_token_set) + { + if (RAND_bytes(s_bind_token, sizeof(s_bind_token)) != 1) + { + die("SSL: RAND_bytes failed"); + } + s_bind_token_set = 1; + } + p_ssl = get_ssl(p_sess, VSFTP_COMMAND_FD); if (p_ssl == NULL) { return 0; } + /* Covers session ID based resumption via the server side session cache, + * where no ticket (and thus no ticket callback) is involved. The cached + * session object is the same as the one attached to p_ssl. + */ + if (!SSL_SESSION_set1_ticket_appdata(SSL_get_session(p_ssl), s_bind_token, + sizeof(s_bind_token))) + { + die("SSL: could not bind control session"); + } p_sess->p_control_ssl = p_ssl; (void) ssl_cert_digest(p_ssl, p_sess, &p_sess->control_cert_digest); setup_bio_callbacks(p_ssl); @@ -649,6 +706,77 @@ ssl_session_init(struct vsf_session* p_s } static int +ssl_session_is_bound(const SSL_SESSION* p_ssl_sess) +{ + void* p_data = NULL; + size_t len = 0; + if (p_ssl_sess == NULL || !s_bind_token_set) + { + return 0; + } + if (!SSL_SESSION_get0_ticket_appdata((SSL_SESSION*) p_ssl_sess, &p_data, + &len)) + { + return 0; + } + if (p_data == NULL || len != sizeof(s_bind_token)) + { + return 0; + } + return CRYPTO_memcmp(p_data, s_bind_token, sizeof(s_bind_token)) == 0; +} + +static int +ssl_ticket_gen_callback(SSL* p_ssl, void* p_arg) +{ + (void) p_arg; + /* Tickets issued on the control channel get this process' token. Tickets + * issued on a data channel keep whatever the (possibly resumed) session + * already carries, so a fresh data channel session never gets bound. + */ + if (SSL_get_fd(p_ssl) == VSFTP_COMMAND_FD && s_bind_token_set) + { + return SSL_SESSION_set1_ticket_appdata(SSL_get_session(p_ssl), + s_bind_token, sizeof(s_bind_token)); + } + return 1; +} + +static SSL_TICKET_RETURN +ssl_ticket_dec_callback(SSL* p_ssl, + SSL_SESSION* p_ssl_sess, + const unsigned char* p_keyname, + size_t keyname_len, + SSL_TICKET_STATUS status, + void* p_arg) +{ + (void) p_ssl; + (void) p_keyname; + (void) keyname_len; + (void) p_arg; + switch (status) + { + case SSL_TICKET_SUCCESS: + case SSL_TICKET_SUCCESS_RENEW: + /* Refuse to resume tickets minted by other session processes; this + * forces a full handshake, which require_ssl_reuse then rejects on + * the data channel. + */ + if (!ssl_session_is_bound(p_ssl_sess)) + { + return SSL_TICKET_RETURN_IGNORE_RENEW; + } + return (status == SSL_TICKET_SUCCESS) ? SSL_TICKET_RETURN_USE + : SSL_TICKET_RETURN_USE_RENEW; + case SSL_TICKET_FATAL_ERR_MALLOC: + case SSL_TICKET_FATAL_ERR_OTHER: + return SSL_TICKET_RETURN_ABORT; + default: + return SSL_TICKET_RETURN_IGNORE_RENEW; + } +} + +static int ssl_cert_digest(SSL* p_ssl, struct vsf_session* p_sess, struct mystr* p_str) { X509* p_cert = SSL_get_peer_certificate(p_ssl); ++++++ vsftpd-no-stderr-on-control-connection.patch ++++++ From: Peter Simons <[email protected]> Subject: Never send stderr output to the FTP client In standalone mode the session process got the client socket on fds 0, 1 and 2, and in inetd mode the super server usually does the same. vsftpd itself only talks to the client through fd 0, but library code it calls into prints diagnostics to stderr. For example, libnsl prints "yp_bind_client_create_v3: RPC: Unable to send" when ypbind is not reachable during a getpwnam() lookup. That text was injected into the control connection, which confuses clients and leaks server internals. Standalone mode no longer puts the client socket on fd 2, so session processes inherit the listener's stderr instead (the journal when running in the foreground under systemd, /dev/null in background mode). In addition, if fd 1 or 2 refers to the client socket (inetd mode) or is closed, it is pointed at /dev/null before any NSS, PAM or libwrap code runs. --- a/main.c +++ b/main.c @@ -156,6 +156,11 @@ main(int argc, const char* argv[]) the_session.num_this_ip = ret.num_this_ip; } + /* Make sure stdout / stderr do not point to the client connection before + * we call into any library code that might print to them. + */ + vsf_sysutil_detach_stdio_from_socket(VSFTP_COMMAND_FD); + die_init(&the_session); if (tunable_tcp_wrappers) --- a/standalone.c +++ b/standalone.c @@ -201,10 +201,13 @@ vsf_standalone_main(void) static void prepare_child(int new_client_sock) { - /* We must satisfy the contract: command socket on fd 0, 1, 2 */ + /* We must satisfy the contract: command socket on fd 0, 1. Do not put it + * on fd 2: stderr is inherited from the listener (/dev/null in background + * mode, otherwise e.g. the journal), so that diagnostics printed by library + * code end up there instead of being sent to the FTP client. + */ vsf_sysutil_dupfd2(new_client_sock, 0); vsf_sysutil_dupfd2(new_client_sock, 1); - vsf_sysutil_dupfd2(new_client_sock, 2); if (new_client_sock > 2) { vsf_sysutil_close(new_client_sock); --- a/sysutil.c +++ b/sysutil.c @@ -2555,6 +2555,62 @@ error: } void +vsf_sysutil_detach_stdio_from_socket(int sock_fd) +{ + /* stdout and stderr must never refer to the client connection. Library + * code we call into (NSS modules such as NIS, PAM modules, libwrap, ...) + * may print diagnostics there, which would then end up being injected into + * the FTP control connection. This happens in inetd mode, where the super + * server typically passes the connected socket on fds 0, 1 and 2. + */ + struct stat sock_stat; + int fd; + int null_fd = -1; + if (fstat(sock_fd, &sock_stat) != 0) + { + /* Leave reporting this to the regular sanity checks. */ + return; + } + for (fd = STDOUT_FILENO; fd <= STDERR_FILENO; ++fd) + { + struct stat fd_stat; + if (fd == sock_fd) + { + continue; + } + if (fstat(fd, &fd_stat) == 0) + { + if (fd_stat.st_dev != sock_stat.st_dev || + fd_stat.st_ino != sock_stat.st_ino) + { + /* Something else, e.g. the journal or a log file - keep it. */ + continue; + } + } + else if (errno != EBADF) + { + continue; + } + /* Either the client socket, or closed. A closed fd would be handed out + * by the next open()/socket() call, so plug it with /dev/null as well. + */ + if (null_fd < 0) + { + null_fd = open("/dev/null", O_WRONLY, 0); + if (null_fd < 0) + { + die("open /dev/null"); + } + } + vsf_sysutil_dupfd2(null_fd, fd); + } + if (null_fd > STDERR_FILENO) + { + vsf_sysutil_close(null_fd); + } +} + +void vsf_sysutil_tzset(void) { int retval; --- a/sysutil.h +++ b/sysutil.h @@ -303,6 +303,7 @@ unsigned int vsf_sysutil_get_umask(void) void vsf_sysutil_set_umask(unsigned int umask); void vsf_sysutil_make_session_leader(void); void vsf_sysutil_reopen_standard_fds(void); +void vsf_sysutil_detach_stdio_from_socket(int sock_fd); void vsf_sysutil_tzset(void); const char* vsf_sysutil_get_current_date(void); void vsf_sysutil_qsort(void* p_base, unsigned int num_elem, ++++++ [email protected] ++++++ --- /var/tmp/diff_new_pack.p8BBoE/_old 2026-11-10 16:29:55.469548347 +0100 +++ /var/tmp/diff_new_pack.p8BBoE/_new 2026-11-10 16:29:55.473548514 +0100 @@ -5,5 +5,6 @@ Type=simple ExecStart=/usr/sbin/vsftpd /etc/vsftpd.conf -obackground=NO -olisten=NO -olisten_ipv6=NO StandardInput=socket +StandardError=journal SuccessExitStatus=2
