From a5eb30dbfd8f3526b2d04bd9f0a3803b665f5798 Mon Sep 17 00:00:00 2001 From: Jakub Jelen Date: Thu, 11 Dec 2025 17:33:19 +0100 Subject: [PATCH] CVE-2026-0965 config: Do not attempt to read non-regular and too large configuration files Changes also the reading of known_hosts to use the new helper function Signed-off-by: Jakub Jelen Reviewed-by: Andreas Schneider --- include/libssh/misc.h | 3 ++ include/libssh/priv.h | 3 ++ src/bind_config.c | 4 +- src/config.c | 8 ++-- src/dh-gex.c | 4 +- src/known_hosts.c | 2 +- src/knownhosts.c | 2 +- src/misc.c | 74 ++++++++++++++++++++++++++++++++ src/options.c | 11 +++-- tests/unittests/torture_config.c | 20 +++++++++ 10 files changed, 118 insertions(+), 13 deletions(-) diff --git a/include/libssh/misc.h b/include/libssh/misc.h index ed2adbb5..2a241276 100644 --- a/include/libssh/misc.h +++ b/include/libssh/misc.h @@ -36,6 +36,7 @@ #include #include #endif /* _WIN32 */ +#include #ifdef __cplusplus extern "C" { @@ -137,6 +138,8 @@ int ssh_check_username_syntax(const char *username); void ssh_proxyjumps_free(struct ssh_list *proxy_jump_list); bool ssh_libssh_proxy_jumps(void); +FILE *ssh_strict_fopen(const char *filename, size_t max_file_size); + #ifdef __cplusplus } #endif diff --git a/include/libssh/priv.h b/include/libssh/priv.h index 1666f896..56b84c5d 100644 --- a/include/libssh/priv.h +++ b/include/libssh/priv.h @@ -508,6 +508,9 @@ char *ssh_strerror(int err_num, char *buf, size_t buflen); #define SSH_TTY_MODES_MAX_BUFSIZE (55 * 5 + 1) int encode_current_tty_opts(unsigned char *buf, size_t buflen); +/** The default maximum file size for a configuration file */ +#define SSH_MAX_CONFIG_FILE_SIZE 16 * 1024 * 1024 + #ifdef __cplusplus } #endif diff --git a/src/bind_config.c b/src/bind_config.c index f3b91e04..a8bf3703 100644 --- a/src/bind_config.c +++ b/src/bind_config.c @@ -217,7 +217,7 @@ local_parse_file(ssh_bind bind, return; } - f = fopen(filename, "r"); + f = ssh_strict_fopen(filename, SSH_MAX_CONFIG_FILE_SIZE); if (f == NULL) { SSH_LOG(SSH_LOG_RARE, "Cannot find file %s to load", filename); @@ -655,7 +655,7 @@ int ssh_bind_config_parse_file(ssh_bind bind, const char *filename) * option to be redefined later by another file. */ uint8_t seen[BIND_CFG_MAX] = {0}; - f = fopen(filename, "r"); + f = ssh_strict_fopen(filename, SSH_MAX_CONFIG_FILE_SIZE); if (f == NULL) { return 0; } diff --git a/src/config.c b/src/config.c index 33d79329..0216e453 100644 --- a/src/config.c +++ b/src/config.c @@ -256,10 +256,9 @@ local_parse_file(ssh_session session, return; } - f = fopen(filename, "r"); + f = ssh_strict_fopen(filename, SSH_MAX_CONFIG_FILE_SIZE); if (f == NULL) { - SSH_LOG(SSH_LOG_RARE, "Cannot find file %s to load", - filename); + /* The underlying function logs the reasons */ return; } @@ -1708,8 +1707,9 @@ int ssh_config_parse_file(ssh_session session, const char *filename) int rv; bool global = 0; - fp = fopen(filename, "r"); + fp = ssh_strict_fopen(filename, SSH_MAX_CONFIG_FILE_SIZE); if (fp == NULL) { + /* The underlying function logs the reasons */ return 0; } diff --git a/src/dh-gex.c b/src/dh-gex.c index 3c8e5e87..f9fe3cd6 100644 --- a/src/dh-gex.c +++ b/src/dh-gex.c @@ -526,9 +526,9 @@ static int ssh_retrieve_dhgroup(char *moduli_file, } if (moduli_file != NULL) - moduli = fopen(moduli_file, "r"); + moduli = ssh_strict_fopen(moduli_file, SSH_MAX_CONFIG_FILE_SIZE); else - moduli = fopen(MODULI_FILE, "r"); + moduli = ssh_strict_fopen(MODULI_FILE, SSH_MAX_CONFIG_FILE_SIZE); if (moduli == NULL) { char err_msg[SSH_ERRNO_MSG_MAX] = {0}; diff --git a/src/known_hosts.c b/src/known_hosts.c index 3ef83e21..701576ce 100644 --- a/src/known_hosts.c +++ b/src/known_hosts.c @@ -83,7 +83,7 @@ static struct ssh_tokens_st *ssh_get_knownhost_line(FILE **file, struct ssh_tokens_st *tokens = NULL; if (*file == NULL) { - *file = fopen(filename,"r"); + *file = ssh_strict_fopen(filename, SSH_MAX_CONFIG_FILE_SIZE); if (*file == NULL) { return NULL; } diff --git a/src/knownhosts.c b/src/knownhosts.c index 2807e17d..43eecf90 100644 --- a/src/knownhosts.c +++ b/src/knownhosts.c @@ -243,7 +243,7 @@ static int ssh_known_hosts_read_entries(const char *match, FILE *fp = NULL; int rc; - fp = fopen(filename, "r"); + fp = ssh_strict_fopen(filename, SSH_MAX_CONFIG_FILE_SIZE); if (fp == NULL) { char err_msg[SSH_ERRNO_MSG_MAX] = {0}; SSH_LOG(SSH_LOG_TRACE, "Failed to open the known_hosts file '%s': %s", diff --git a/src/misc.c b/src/misc.c index 4a01f908..191a540a 100644 --- a/src/misc.c +++ b/src/misc.c @@ -37,6 +37,7 @@ #endif /* _WIN32 */ #include +#include #include #include #include @@ -2416,4 +2417,77 @@ ssh_libssh_proxy_jumps(void) return !(t != NULL && t[0] == '1'); } +/** + * @internal + * + * @brief Safely open a file containing some configuration. + * + * Runs checks if the file can be used as some configuration file (is regular + * file and is not too large). If so, returns the opened file (for reading). + * Otherwise logs error and returns `NULL`. + * + * @param filename The path to the file to open. + * @param max_file_size Maximum file size that is accepted. + * + * @returns the opened file or `NULL` on error. + */ +FILE *ssh_strict_fopen(const char *filename, size_t max_file_size) +{ + FILE *f = NULL; + struct stat sb; + char err_msg[SSH_ERRNO_MSG_MAX] = {0}; + int r, fd; + + /* open first to avoid TOCTOU */ + fd = open(filename, O_RDONLY); + if (fd == -1) { + SSH_LOG(SSH_LOG_RARE, + "Failed to open a file %s for reading: %s", + filename, + ssh_strerror(errno, err_msg, SSH_ERRNO_MSG_MAX)); + return NULL; + } + + /* Check the file is sensible for a configuration file */ + r = fstat(fd, &sb); + if (r != 0) { + SSH_LOG(SSH_LOG_RARE, + "Failed to stat %s: %s", + filename, + ssh_strerror(errno, err_msg, SSH_ERRNO_MSG_MAX)); + close(fd); + return NULL; + } + if ((sb.st_mode & S_IFMT) != S_IFREG) { + SSH_LOG(SSH_LOG_RARE, + "The file %s is not a regular file: skipping", + filename); + close(fd); + return NULL; + } + + if ((size_t)sb.st_size > max_file_size) { + SSH_LOG(SSH_LOG_RARE, + "The file %s is too large (%jd MB > %zu MB): skipping", + filename, + (intmax_t)sb.st_size / 1024 / 1024, + max_file_size / 1024 / 1024); + close(fd); + return NULL; + } + + f = fdopen(fd, "r"); + if (f == NULL) { + SSH_LOG(SSH_LOG_RARE, + "Failed to open a file %s for reading: %s", + filename, + ssh_strerror(r, err_msg, SSH_ERRNO_MSG_MAX)); + close(fd); + return NULL; + } + + /* the flcose() will close also the underlying fd */ + return f; +} + /** @} */ diff --git a/src/options.c b/src/options.c index 8125903f..e5e75589 100644 --- a/src/options.c +++ b/src/options.c @@ -2008,11 +2008,16 @@ int ssh_options_parse_config(ssh_session session, const char *filename) goto out; } if (filename == NULL) { - if ((fp = fopen(GLOBAL_CLIENT_CONFIG, "r")) != NULL) { + fp = ssh_strict_fopen(GLOBAL_CLIENT_CONFIG, SSH_MAX_CONFIG_FILE_SIZE); + if (fp != NULL) { filename = GLOBAL_CLIENT_CONFIG; #ifdef USR_GLOBAL_CLIENT_CONFIG - } else if ((fp = fopen(USR_GLOBAL_CLIENT_CONFIG, "r")) != NULL) { - filename = USR_GLOBAL_CLIENT_CONFIG; + } else { + fp = ssh_strict_fopen(USR_GLOBAL_CLIENT_CONFIG, + SSH_MAX_CONFIG_FILE_SIZE); + if (fp != NULL) { + filename = USR_GLOBAL_CLIENT_CONFIG; + } #endif } diff --git a/tests/unittests/torture_config.c b/tests/unittests/torture_config.c index 6112e7a3..7ac9d9dc 100644 --- a/tests/unittests/torture_config.c +++ b/tests/unittests/torture_config.c @@ -2933,6 +2933,23 @@ static void torture_config_jump(void **state) printf("%s: EOF\n", __func__); } +/* Invalid configuration files + */ +static void torture_config_invalid(void **state) +{ + ssh_session session = *state; + + ssh_options_set(session, SSH_OPTIONS_HOST, "Bar"); + + /* non-regular file -- ignored (or missing on non-unix) so OK */ + _parse_config(session, "/dev/random", NULL, SSH_OK); + +#ifndef _WIN32 + /* huge file -- ignored (or missing on non-unix) so OK */ + _parse_config(session, "/proc/kcore", NULL, SSH_OK); +#endif +} + int torture_run_tests(void) { int rc; @@ -3087,6 +3104,9 @@ int torture_run_tests(void) cmocka_unit_test_setup_teardown(torture_config_jump, setup, teardown), + cmocka_unit_test_setup_teardown(torture_config_invalid, + setup, + teardown), }; ssh_init();