diff --git a/libssh-CVE-2026-0964-scp-Reject-invalid-paths-received-thro.patch b/libssh-CVE-2026-0964-scp-Reject-invalid-paths-received-thro.patch new file mode 100644 index 0000000..befd686 --- /dev/null +++ b/libssh-CVE-2026-0964-scp-Reject-invalid-paths-received-thro.patch @@ -0,0 +1,40 @@ +From a5e4b12090b0c939d85af4f29280e40c5b6600aa Mon Sep 17 00:00:00 2001 +From: Jakub Jelen +Date: Mon, 22 Dec 2025 19:16:44 +0100 +Subject: [PATCH 09/12] CVE-2026-0964 scp: Reject invalid paths received + through scp + +Signed-off-by: Jakub Jelen +Reviewed-by: Andreas Schneider +(cherry picked from commit daa80818f89347b4d80b0c5b80659f9a9e55e8cc) + +diff --git a/src/scp.c b/src/scp.c +index a1e3687f..08a29fad 100644 +--- a/src/scp.c ++++ b/src/scp.c +@@ -862,6 +862,22 @@ int ssh_scp_pull_request(ssh_scp scp) + size = strtoull(tmp, NULL, 10); + p++; + name = strdup(p); ++ /* Catch invalid name: ++ * - empty ones ++ * - containing any forward slash -- directory traversal handled ++ * differently ++ * - special names "." and ".." referring to the current and parent ++ * directories -- they are not expected either ++ */ ++ if (name == NULL || name[0] == '\0' || strchr(name, '/') || ++ strcmp(name, ".") == 0 || strcmp(name, "..") == 0) { ++ ssh_set_error(scp->session, ++ SSH_FATAL, ++ "Received invalid filename: %s", ++ name == NULL ? "" : name); ++ SAFE_FREE(name); ++ goto error; ++ } + SAFE_FREE(scp->request_name); + scp->request_name = name; + if (buffer[0] == 'C') { +-- +2.52.0 + diff --git a/libssh-CVE-2026-0965-config-Do-not-attempt-to-read-non-regu.patch b/libssh-CVE-2026-0965-config-Do-not-attempt-to-read-non-regu.patch new file mode 100644 index 0000000..1a5d6ea --- /dev/null +++ b/libssh-CVE-2026-0965-config-Do-not-attempt-to-read-non-regu.patch @@ -0,0 +1,267 @@ +From bf390a042623e02abc8f421c4c5fadc0429a8a76 Mon Sep 17 00:00:00 2001 +From: Jakub Jelen +Date: Thu, 11 Dec 2025 17:33:19 +0100 +Subject: [PATCH 08/12] 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 +(cherry picked from commit a5eb30dbfd8f3526b2d04bd9f0a3803b665f5798) + +Index: libssh-0.10.6/include/libssh/misc.h +=================================================================== +--- libssh-0.10.6.orig/include/libssh/misc.h ++++ libssh-0.10.6/include/libssh/misc.h +@@ -21,6 +21,8 @@ + #ifndef MISC_H_ + #define MISC_H_ + ++#include ++ + #ifdef __cplusplus + extern "C" { + #endif +@@ -106,6 +108,8 @@ char *ssh_strreplace(const char *src, co + + int ssh_check_hostname_syntax(const char *hostname); + ++FILE *ssh_strict_fopen(const char *filename, size_t max_file_size); ++ + #ifdef __cplusplus + } + #endif +Index: libssh-0.10.6/include/libssh/priv.h +=================================================================== +--- libssh-0.10.6.orig/include/libssh/priv.h ++++ libssh-0.10.6/include/libssh/priv.h +@@ -438,6 +438,9 @@ bool is_ssh_initialized(void); + #define SSH_ERRNO_MSG_MAX 1024 + char *ssh_strerror(int err_num, 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 +Index: libssh-0.10.6/src/bind_config.c +=================================================================== +--- libssh-0.10.6.orig/src/bind_config.c ++++ libssh-0.10.6/src/bind_config.c +@@ -212,7 +212,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); +@@ -636,7 +636,7 @@ int ssh_bind_config_parse_file(ssh_bind + * 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; + } +Index: libssh-0.10.6/src/config.c +=================================================================== +--- libssh-0.10.6.orig/src/config.c ++++ libssh-0.10.6/src/config.c +@@ -215,10 +215,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; + } + +@@ -1205,8 +1204,9 @@ int ssh_config_parse_file(ssh_session se + int parsing, rv; + bool global = 0; + +- f = fopen(filename, "r"); ++ f = ssh_strict_fopen(filename, SSH_MAX_CONFIG_FILE_SIZE); + if (f == NULL) { ++ /* The underlying function logs the reasons */ + return 0; + } + +Index: libssh-0.10.6/src/dh-gex.c +=================================================================== +--- libssh-0.10.6.orig/src/dh-gex.c ++++ libssh-0.10.6/src/dh-gex.c +@@ -520,9 +520,9 @@ static int ssh_retrieve_dhgroup(char *mo + } + + 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}; +Index: libssh-0.10.6/src/known_hosts.c +=================================================================== +--- libssh-0.10.6.orig/src/known_hosts.c ++++ libssh-0.10.6/src/known_hosts.c +@@ -83,7 +83,7 @@ static struct ssh_tokens_st *ssh_get_kno + 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; + } +Index: libssh-0.10.6/src/knownhosts.c +=================================================================== +--- libssh-0.10.6.orig/src/knownhosts.c ++++ libssh-0.10.6/src/knownhosts.c +@@ -232,7 +232,7 @@ static int ssh_known_hosts_read_entries( + 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_WARN, "Failed to open the known_hosts file '%s': %s", +Index: libssh-0.10.6/src/misc.c +=================================================================== +--- libssh-0.10.6.orig/src/misc.c ++++ libssh-0.10.6/src/misc.c +@@ -37,6 +37,7 @@ + #endif /* _WIN32 */ + + #include ++#include + #include + #include + #include +@@ -2074,4 +2075,77 @@ int ssh_check_hostname_syntax(const char + return SSH_OK; + } + ++/** ++ * @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; ++} ++ + /** @} */ +Index: libssh-0.10.6/tests/unittests/torture_config.c +=================================================================== +--- libssh-0.10.6.orig/tests/unittests/torture_config.c ++++ libssh-0.10.6/tests/unittests/torture_config.c +@@ -1955,6 +1955,23 @@ static void torture_config_parse_uri(voi + SAFE_FREE(hostname); + } + ++/* 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; +@@ -2029,6 +2046,8 @@ int torture_run_tests(void) + setup_no_sshdir, teardown), + cmocka_unit_test_setup_teardown(torture_config_parse_uri, + setup, teardown), ++ cmocka_unit_test_setup_teardown(torture_config_invalid, ++ setup, teardown), + }; + + diff --git a/libssh-CVE-2026-0966-doc-Update-guided-tour-to-use-SHA256-f.patch b/libssh-CVE-2026-0966-doc-Update-guided-tour-to-use-SHA256-f.patch new file mode 100644 index 0000000..099f265 --- /dev/null +++ b/libssh-CVE-2026-0966-doc-Update-guided-tour-to-use-SHA256-f.patch @@ -0,0 +1,59 @@ +From 3e1d276a5a030938a8f144f46ff4f2a2efe31ced Mon Sep 17 00:00:00 2001 +From: Jakub Jelen +Date: Thu, 8 Jan 2026 12:10:44 +0100 +Subject: [PATCH 07/12] CVE-2026-0966 doc: Update guided tour to use SHA256 + fingerprints +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Signed-off-by: Jakub Jelen +Reviewed-by: Pavol Žáčik +(cherry picked from commit 1b2a4f760bec35121c490f2294f915ebb9c992ae) + +diff --git a/doc/guided_tour.dox b/doc/guided_tour.dox +index 1a41d6cb..e98b5e0d 100644 +--- a/doc/guided_tour.dox ++++ b/doc/guided_tour.dox +@@ -190,7 +190,6 @@ int verify_knownhost(ssh_session session) + ssh_key srv_pubkey = NULL; + size_t hlen; + char buf[10]; +- char *hexa = NULL; + char *p = NULL; + int cmp; + int rc; +@@ -201,7 +200,7 @@ int verify_knownhost(ssh_session session) + } + + rc = ssh_get_publickey_hash(srv_pubkey, +- SSH_PUBLICKEY_HASH_SHA1, ++ SSH_PUBLICKEY_HASH_SHA256, + &hash, + &hlen); + ssh_key_free(srv_pubkey); +@@ -217,7 +216,7 @@ int verify_knownhost(ssh_session session) + break; + case SSH_KNOWN_HOSTS_CHANGED: + fprintf(stderr, "Host key for server changed: it is now:\n"); +- ssh_print_hexa("Public key hash", hash, hlen); ++ ssh_print_hash(SSH_PUBLICKEY_HASH_SHA256, hash, hlen); + fprintf(stderr, "For security reasons, connection will be stopped\n"); + ssh_clean_pubkey_hash(&hash); + +@@ -238,10 +237,9 @@ int verify_knownhost(ssh_session session) + /* FALL THROUGH to SSH_SERVER_NOT_KNOWN behavior */ + + case SSH_KNOWN_HOSTS_UNKNOWN: +- hexa = ssh_get_hexa(hash, hlen); + fprintf(stderr,"The server is unknown. Do you trust the host key?\n"); +- fprintf(stderr, "Public key hash: %s\n", hexa); +- ssh_string_free_char(hexa); ++ fprintf(stderr, "Public key hash: "); ++ ssh_print_hash(SSH_PUBLICKEY_HASH_SHA256, hash, hlen); + ssh_clean_pubkey_hash(&hash); + p = fgets(buf, sizeof(buf), stdin); + if (p == NULL) { +-- +2.52.0 + diff --git a/libssh-CVE-2026-0966-misc-Avoid-heap-buffer-underflow-in-ss.patch b/libssh-CVE-2026-0966-misc-Avoid-heap-buffer-underflow-in-ss.patch new file mode 100644 index 0000000..ab6b03f --- /dev/null +++ b/libssh-CVE-2026-0966-misc-Avoid-heap-buffer-underflow-in-ss.patch @@ -0,0 +1,29 @@ +From 6ba5ff1b7b1547a59f750fbc06b89737b7456117 Mon Sep 17 00:00:00 2001 +From: Jakub Jelen +Date: Thu, 8 Jan 2026 12:09:50 +0100 +Subject: [PATCH 05/12] CVE-2026-0966 misc: Avoid heap buffer underflow in + ssh_get_hexa +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Signed-off-by: Jakub Jelen +Reviewed-by: Pavol Žáčik +(cherry picked from commit 417a095e6749a1f3635e02332061edad3c6a3401) + +diff --git a/src/misc.c b/src/misc.c +index 774211fb..08c690b2 100644 +--- a/src/misc.c ++++ b/src/misc.c +@@ -459,7 +459,7 @@ char *ssh_get_hexa(const unsigned char *what, size_t len) + size_t i; + size_t hlen = len * 3; + +- if (len > (UINT_MAX - 1) / 3) { ++ if (what == NULL || len < 1 || len > (UINT_MAX - 1) / 3) { + return NULL; + } + +-- +2.52.0 + diff --git a/libssh-CVE-2026-0966-tests-Test-coverage-for-ssh_get_hexa.patch b/libssh-CVE-2026-0966-tests-Test-coverage-for-ssh_get_hexa.patch new file mode 100644 index 0000000..1490687 --- /dev/null +++ b/libssh-CVE-2026-0966-tests-Test-coverage-for-ssh_get_hexa.patch @@ -0,0 +1,61 @@ +From b156391833c66322436cf177d57e10b0325fbcc8 Mon Sep 17 00:00:00 2001 +From: Jakub Jelen +Date: Thu, 8 Jan 2026 12:10:16 +0100 +Subject: [PATCH 06/12] CVE-2026-0966 tests: Test coverage for ssh_get_hexa +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Signed-off-by: Jakub Jelen +Reviewed-by: Pavol Žáčik +(cherry picked from commit 9be83584a56580da5a2f41e47137056dc0249b52) + +Index: libssh-0.10.6/tests/unittests/torture_misc.c +=================================================================== +--- libssh-0.10.6.orig/tests/unittests/torture_misc.c ++++ libssh-0.10.6/tests/unittests/torture_misc.c +@@ -881,6 +881,36 @@ static void torture_ssh_is_ipaddr(void * + assert_int_equal(rc, 0); + } + ++static void torture_ssh_get_hexa(void **state) ++{ ++ const unsigned char *bin = NULL; ++ char *hex = NULL; ++ ++ (void)state; ++ ++ /* Null pointer should not crash */ ++ bin = NULL; ++ hex = ssh_get_hexa(bin, 0); ++ assert_null(hex); ++ ++ /* Null pointer should not crash regardless the length */ ++ bin = NULL; ++ hex = ssh_get_hexa(bin, 99); ++ assert_null(hex); ++ ++ /* Zero length input is not much useful. Just expect NULL too */ ++ bin = (const unsigned char *)""; ++ hex = ssh_get_hexa(bin, 0); ++ assert_null(hex); ++ ++ /* Valid inputs */ ++ bin = (const unsigned char *)"\x00\xFF"; ++ hex = ssh_get_hexa(bin, 2); ++ assert_non_null(hex); ++ assert_string_equal(hex, "00:ff"); ++ ssh_string_free_char(hex); ++} ++ + int torture_run_tests(void) { + int rc; + struct CMUnitTest tests[] = { +@@ -907,6 +937,7 @@ int torture_run_tests(void) { + cmocka_unit_test(torture_ssh_strerror), + cmocka_unit_test(torture_ssh_check_hostname_syntax), + cmocka_unit_test(torture_ssh_is_ipaddr), ++ cmocka_unit_test(torture_ssh_get_hexa), + }; + + ssh_init(); diff --git a/libssh-CVE-2026-0967-match-Avoid-recursive-matching-ReDoS.patch b/libssh-CVE-2026-0967-match-Avoid-recursive-matching-ReDoS.patch new file mode 100644 index 0000000..1cf470f --- /dev/null +++ b/libssh-CVE-2026-0967-match-Avoid-recursive-matching-ReDoS.patch @@ -0,0 +1,353 @@ +From 6d74aa6138895b3662bade9bd578338b0c4f8a15 Mon Sep 17 00:00:00 2001 +From: Jakub Jelen +Date: Wed, 17 Dec 2025 18:48:34 +0100 +Subject: [PATCH 04/12] CVE-2026-0967 match: Avoid recursive matching (ReDoS) +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +The specially crafted patterns (from configuration files) could cause +exhaustive search or timeouts. + +Previous attempts to fix this by limiting recursion to depth 16 avoided +stack overflow, but not timeouts. This is due to the backtracking, +which caused the exponential time complexity O(N^16) of existing algorithm. + +This is code comes from the same function from OpenSSH, where this code +originates from, which is not having this issue (due to not limiting the number +of recursion), but will also easily exhaust stack due to unbound recursion: + +https://github.com/openssh/openssh-portable/commit/05bcd0cadf160fd4826a2284afa7cba6ec432633 + +This is an attempt to simplify the algorithm by preventing the backtracking +to previous wildcard, which should keep the same behavior for existing inputs +while reducing the complexity to linear O(N*M). + +This fixes the long-term issue we had with fuzzing as well as recently reported +security issue by Kang Yang. + +Signed-off-by: Jakub Jelen +Reviewed-by: Pavol Žáčik +(cherry picked from commit a411de5ce806e3ea24d088774b2f7584d6590b5f) + +diff --git a/src/match.c b/src/match.c +index 2c004c98..771ee63c 100644 +--- a/src/match.c ++++ b/src/match.c +@@ -53,85 +53,70 @@ + + #include "libssh/priv.h" + +-#define MAX_MATCH_RECURSION 16 +- +-/* +- * Returns true if the given string matches the pattern (which may contain ? +- * and * as wildcards), and zero if it does not match. ++/** ++ * @brief Compare a string with a pattern containing wildcards `*` and `?` ++ * ++ * This function is an iterative replacement for the previously recursive ++ * implementation to avoid exponential complexity (DoS) with specific patterns. ++ * ++ * @param[in] s The string to match. ++ * @param[in] pattern The pattern to match against. ++ * ++ * @return 1 if the pattern matches, 0 otherwise. + */ +-static int match_pattern(const char *s, const char *pattern, size_t limit) ++static int match_pattern(const char *s, const char *pattern) + { +- bool had_asterisk = false; ++ const char *s_star = NULL; /* Position in s when last `*` was met */ ++ const char *p_star = NULL; /* Position in pattern after last `*` */ + +- if (s == NULL || pattern == NULL || limit <= 0) { ++ if (s == NULL || pattern == NULL) { + return 0; + } + +- for (;;) { +- /* If at end of pattern, accept if also at end of string. */ +- if (*pattern == '\0') { +- return (*s == '\0'); +- } +- +- /* Skip all the asterisks and adjacent question marks */ +- while (*pattern == '*' || (had_asterisk && *pattern == '?')) { +- if (*pattern == '*') { +- had_asterisk = true; +- } ++ while (*s) { ++ /* Case 1: Exact match or '?' wildcard */ ++ if (*pattern == *s || *pattern == '?') { ++ s++; + pattern++; ++ continue; + } + +- if (had_asterisk) { +- /* If at end of pattern, accept immediately. */ +- if (!*pattern) +- return 1; +- +- /* If next character in pattern is known, optimize. */ +- if (*pattern != '?') { +- /* +- * Look instances of the next character in +- * pattern, and try to match starting from +- * those. +- */ +- for (; *s; s++) +- if (*s == *pattern && match_pattern(s + 1, pattern + 1, limit - 1)) { +- return 1; +- } +- /* Failed. */ +- return 0; +- } +- /* +- * Move ahead one character at a time and try to +- * match at each position. ++ /* Case 2: '*' wildcard */ ++ if (*pattern == '*') { ++ /* Record the position of the star and the current string position. ++ * We optimistically assume * matches 0 characters first. + */ +- for (; *s; s++) { +- if (match_pattern(s, pattern, limit - 1)) { +- return 1; +- } +- } +- /* Failed. */ +- return 0; +- } +- /* +- * There must be at least one more character in the string. +- * If we are at the end, fail. +- */ +- if (!*s) { +- return 0; ++ p_star = ++pattern; ++ s_star = s; ++ continue; + } + +- /* Check if the next character of the string is acceptable. */ +- if (*pattern != '?' && *pattern != *s) { +- return 0; ++ /* Case 3: Mismatch */ ++ if (p_star) { ++ /* If we have seen a star previously, backtrack. ++ * We restore the pattern to just after the star, ++ * but advance the string position (consume one more char for the ++ * star). ++ * No need to backtrack to previous stars as any match of the last ++ * star could be eaten the same way by the previous star. ++ */ ++ pattern = p_star; ++ s = ++s_star; ++ continue; + } + +- /* Move to the next character, both in string and in pattern. */ +- s++; ++ /* Case 4: Mismatch and no star to backtrack to */ ++ return 0; ++ } ++ ++ /* Handle trailing stars in the pattern ++ * (e.g., pattern "abc*" matching "abc") */ ++ while (*pattern == '*') { + pattern++; + } + +- /* NOTREACHED */ +- return 0; ++ /* If we reached the end of the pattern, it's a match */ ++ return (*pattern == '\0'); + } + + /* +@@ -182,7 +167,7 @@ int match_pattern_list(const char *string, const char *pattern, + sub[subi] = '\0'; + + /* Try to match the subpattern against the string. */ +- if (match_pattern(string, sub, MAX_MATCH_RECURSION)) { ++ if (match_pattern(string, sub)) { + if (negated) { + return -1; /* Negative */ + } else { +diff --git a/tests/unittests/torture_config.c b/tests/unittests/torture_config.c +index 973758eb..5c908aa5 100644 +--- a/tests/unittests/torture_config.c ++++ b/tests/unittests/torture_config.c +@@ -2372,80 +2372,138 @@ static void torture_config_match_pattern(void **state) + (void) state; + + /* Simple test "a" matches "a" */ +- rv = match_pattern("a", "a", MAX_MATCH_RECURSION); ++ rv = match_pattern("a", "a"); + assert_int_equal(rv, 1); + + /* Simple test "a" does not match "b" */ +- rv = match_pattern("a", "b", MAX_MATCH_RECURSION); ++ rv = match_pattern("a", "b"); + assert_int_equal(rv, 0); + + /* NULL arguments are correctly handled */ +- rv = match_pattern("a", NULL, MAX_MATCH_RECURSION); ++ rv = match_pattern("a", NULL); + assert_int_equal(rv, 0); +- rv = match_pattern(NULL, "a", MAX_MATCH_RECURSION); ++ rv = match_pattern(NULL, "a"); + assert_int_equal(rv, 0); + + /* Simple wildcard ? is handled in pattern */ +- rv = match_pattern("a", "?", MAX_MATCH_RECURSION); ++ rv = match_pattern("a", "?"); + assert_int_equal(rv, 1); +- rv = match_pattern("aa", "?", MAX_MATCH_RECURSION); ++ rv = match_pattern("aa", "?"); + assert_int_equal(rv, 0); + /* Wildcard in search string */ +- rv = match_pattern("?", "a", MAX_MATCH_RECURSION); ++ rv = match_pattern("?", "a"); + assert_int_equal(rv, 0); +- rv = match_pattern("?", "?", MAX_MATCH_RECURSION); ++ rv = match_pattern("?", "?"); + assert_int_equal(rv, 1); + + /* Simple wildcard * is handled in pattern */ +- rv = match_pattern("a", "*", MAX_MATCH_RECURSION); ++ rv = match_pattern("a", "*"); + assert_int_equal(rv, 1); +- rv = match_pattern("aa", "*", MAX_MATCH_RECURSION); ++ rv = match_pattern("aa", "*"); + assert_int_equal(rv, 1); + /* Wildcard in search string */ +- rv = match_pattern("*", "a", MAX_MATCH_RECURSION); ++ rv = match_pattern("*", "a"); + assert_int_equal(rv, 0); +- rv = match_pattern("*", "*", MAX_MATCH_RECURSION); ++ rv = match_pattern("*", "*"); + assert_int_equal(rv, 1); + + /* More complicated patterns */ +- rv = match_pattern("a", "*a", MAX_MATCH_RECURSION); ++ rv = match_pattern("a", "*a"); + assert_int_equal(rv, 1); +- rv = match_pattern("a", "a*", MAX_MATCH_RECURSION); ++ rv = match_pattern("a", "a*"); + assert_int_equal(rv, 1); +- rv = match_pattern("abababc", "*abc", MAX_MATCH_RECURSION); ++ rv = match_pattern("abababc", "*abc"); + assert_int_equal(rv, 1); +- rv = match_pattern("ababababca", "*abc", MAX_MATCH_RECURSION); ++ rv = match_pattern("ababababca", "*abc"); + assert_int_equal(rv, 0); +- rv = match_pattern("ababababca", "*abc*", MAX_MATCH_RECURSION); ++ rv = match_pattern("ababababca", "*abc*"); + assert_int_equal(rv, 1); + + /* Multiple wildcards in row */ +- rv = match_pattern("aa", "??", MAX_MATCH_RECURSION); ++ rv = match_pattern("aa", "??"); + assert_int_equal(rv, 1); +- rv = match_pattern("bba", "??a", MAX_MATCH_RECURSION); ++ rv = match_pattern("bba", "??a"); + assert_int_equal(rv, 1); +- rv = match_pattern("aaa", "**a", MAX_MATCH_RECURSION); ++ rv = match_pattern("aaa", "**a"); + assert_int_equal(rv, 1); +- rv = match_pattern("bbb", "**a", MAX_MATCH_RECURSION); ++ rv = match_pattern("bbb", "**a"); + assert_int_equal(rv, 0); + + /* Consecutive asterisks do not make sense and do not need to recurse */ +- rv = match_pattern("hostname", "**********pattern", 5); ++ rv = match_pattern("hostname", "**********pattern"); + assert_int_equal(rv, 0); +- rv = match_pattern("hostname", "pattern**********", 5); ++ rv = match_pattern("hostname", "pattern**********"); + assert_int_equal(rv, 0); +- rv = match_pattern("pattern", "***********pattern", 5); ++ rv = match_pattern("pattern", "***********pattern"); + assert_int_equal(rv, 1); +- rv = match_pattern("pattern", "pattern***********", 5); ++ rv = match_pattern("pattern", "pattern***********"); + assert_int_equal(rv, 1); + +- /* Limit the maximum recursion */ +- rv = match_pattern("hostname", "*p*a*t*t*e*r*n*", 5); ++ rv = match_pattern("hostname", "*p*a*t*t*e*r*n*"); + assert_int_equal(rv, 0); +- /* Too much recursion */ +- rv = match_pattern("pattern", "*p*a*t*t*e*r*n*", 5); ++ rv = match_pattern("pattern", "*p*a*t*t*e*r*n*"); ++ assert_int_equal(rv, 1); ++ ++ /* Regular Expression Denial of Service */ ++ rv = match_pattern("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", ++ "*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a"); ++ assert_int_equal(rv, 1); ++ rv = match_pattern("ababababababababababababababababababababab", ++ "*a*b*a*b*a*b*a*b*a*b*a*b*a*b*a*b"); ++ assert_int_equal(rv, 1); ++ ++ /* A lot of backtracking */ ++ rv = match_pattern("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaax", ++ "a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*ax"); ++ assert_int_equal(rv, 1); ++ ++ /* Test backtracking: *a matches first 'a', fails on 'b', must backtrack */ ++ rv = match_pattern("axaxaxb", "*a*b"); ++ assert_int_equal(rv, 1); ++ ++ /* Test greedy consumption with suffix */ ++ rv = match_pattern("foo_bar_baz_bar", "*bar"); ++ assert_int_equal(rv, 1); ++ ++ /* Test exact suffix requirement (ensure no partial match acceptance) */ ++ rv = match_pattern("foobar_extra", "*bar"); ++ assert_int_equal(rv, 0); ++ ++ /* Test multiple distinct wildcards */ ++ rv = match_pattern("a_very_long_string_with_a_pattern", "*long*pattern"); ++ assert_int_equal(rv, 1); ++ ++ /* ? inside a * sequence */ ++ rv = match_pattern("abcdefg", "a*c?e*g"); ++ assert_int_equal(rv, 1); ++ ++ /* Consecutive mixed wildcards */ ++ rv = match_pattern("abc", "*?c"); ++ assert_int_equal(rv, 1); ++ ++ /* ? at the very end after * */ ++ rv = match_pattern("abc", "ab?"); ++ assert_int_equal(rv, 1); ++ rv = match_pattern("abc", "ab*?"); ++ assert_int_equal(rv, 1); ++ ++ /* Consecutive stars should be collapsed or handled gracefully */ ++ rv = match_pattern("abc", "a**c"); ++ assert_int_equal(rv, 1); ++ rv = match_pattern("abc", "***"); ++ assert_int_equal(rv, 1); ++ ++ /* Empty string handling */ ++ rv = match_pattern("", "*"); ++ assert_int_equal(rv, 1); ++ rv = match_pattern("", "?"); + assert_int_equal(rv, 0); ++ rv = match_pattern("", ""); ++ assert_int_equal(rv, 1); + ++ /* Pattern longer than string */ ++ rv = match_pattern("short", "short_but_longer"); ++ assert_int_equal(rv, 0); + } + + /* Identity file can be specified multiple times in the configuration +-- +2.52.0 + diff --git a/libssh-CVE-2026-0968-sftp-Sanitize-input-handling-in-sftp_p.patch b/libssh-CVE-2026-0968-sftp-Sanitize-input-handling-in-sftp_p.patch new file mode 100644 index 0000000..b55cc52 --- /dev/null +++ b/libssh-CVE-2026-0968-sftp-Sanitize-input-handling-in-sftp_p.patch @@ -0,0 +1,54 @@ +From 796d85f786dff62bd4bcc4408d9b7bbc855841e9 Mon Sep 17 00:00:00 2001 +From: Jakub Jelen +Date: Mon, 22 Dec 2025 20:59:11 +0100 +Subject: [PATCH 02/12] CVE-2026-0968: sftp: Sanitize input handling in + sftp_parse_longname() + +Signed-off-by: Jakub Jelen +Reviewed-by: Andreas Schneider +(cherry picked from commit 20856f44c146468c830da61dcbbbaa8ce71e390b) + +Index: libssh-0.10.6/src/sftp.c +=================================================================== +--- libssh-0.10.6.orig/src/sftp.c ++++ libssh-0.10.6/src/sftp.c +@@ -1286,16 +1286,21 @@ enum sftp_longname_field_e { + + static char *sftp_parse_longname(const char *longname, + enum sftp_longname_field_e longname_field) { +- const char *p, *q; ++ const char *p = NULL, *q = NULL; + size_t len, field = 0; + ++ if (longname == NULL || longname_field < SFTP_LONGNAME_PERM || ++ longname_field > SFTP_LONGNAME_NAME) { ++ return NULL; ++ } ++ + p = longname; + /* Find the beginning of the field which is specified by sftp_longname_field_e. */ +- while(field != longname_field) { ++ while (*p != '\0' && field != longname_field) { + if(isspace(*p)) { + field++; + p++; +- while(*p && isspace(*p)) { ++ while (*p != '\0' && isspace(*p)) { + p++; + } + } else { +@@ -1303,8 +1308,13 @@ static char *sftp_parse_longname(const c + } + } + ++ /* If we reached NULL before we got our field fail */ ++ if (field != longname_field) { ++ return NULL; ++ } ++ + q = p; +- while (! isspace(*q)) { ++ while (*q != '\0' && !isspace(*q)) { + q++; + } + diff --git a/libssh.changes b/libssh.changes index bb41e84..8ac631f 100644 --- a/libssh.changes +++ b/libssh.changes @@ -1,3 +1,22 @@ +------------------------------------------------------------------- +Wed Feb 11 11:28:10 UTC 2026 - Pedro Monreal + +- Security fixes: + * CVE-2026-0964: SCP Protocol Path Traversal in ssh_scp_pull_request() (bsc#1258049) + * CVE-2026-0965: Possible Denial of Service when parsing unexpected + configuration files (bsc#1258045) + * CVE-2026-0966: Buffer underflow in ssh_get_hexa() on invalid input (bsc#1258054) + * CVE-2026-0967: Specially crafted patterns could cause DoS (bsc#1258081) + * CVE-2026-0968: OOB Read in sftp_parse_longname() (bsc#1258080) + * Add patches: + - libssh-CVE-2026-0964-scp-Reject-invalid-paths-received-thro.patch + - libssh-CVE-2026-0965-config-Do-not-attempt-to-read-non-regu.patch + - libssh-CVE-2026-0966-misc-Avoid-heap-buffer-underflow-in-ss.patch + - libssh-CVE-2026-0966-tests-Test-coverage-for-ssh_get_hexa.patch + - libssh-CVE-2026-0966-doc-Update-guided-tour-to-use-SHA256-f.patch + - libssh-CVE-2026-0967-match-Avoid-recursive-matching-ReDoS.patch + - libssh-CVE-2026-0968-sftp-Sanitize-input-handling-in-sftp_p.patch + ------------------------------------------------------------------- Fri Sep 19 10:37:27 UTC 2025 - Pedro Monreal diff --git a/libssh.spec b/libssh.spec index 822ad8c..df0bf31 100644 --- a/libssh.spec +++ b/libssh.spec @@ -63,6 +63,18 @@ Patch107: libssh-CVE-2025-8114.patch Patch108: libssh-CVE-2025-8277-packet-Adjust-packet-filter-to-work-wh.patch Patch109: libssh-CVE-2025-8277-Fix-memory-leak-of-unused-ephemeral-ke.patch Patch110: libssh-CVE-2025-8277-ecdh-Free-previously-allocated-pubkeys.patch +# PATCH-FIX-UPSTREAM CVE-2026-0964 bsc#1258049 SCP Protocol Path Traversal in ssh_scp_pull_request() +Patch111: libssh-CVE-2026-0964-scp-Reject-invalid-paths-received-thro.patch +# PATCH-FIX-UPSTREAM CVE-2026-0965 bsc#1258045 Denial of Service via improper configuration file handling +Patch112: libssh-CVE-2026-0965-config-Do-not-attempt-to-read-non-regu.patch +# PATCH-FIX-UPSTREAM CVE-2026-0966 bsc#1258054 Buffer underflow in ssh_get_hexa() on invalid input +Patch113: libssh-CVE-2026-0966-misc-Avoid-heap-buffer-underflow-in-ss.patch +Patch114: libssh-CVE-2026-0966-tests-Test-coverage-for-ssh_get_hexa.patch +Patch115: libssh-CVE-2026-0966-doc-Update-guided-tour-to-use-SHA256-f.patch +# PATCH-FIX-UPSTREAM CVE-2026-0967 bsc#1258081 Denial of Service via inefficient regular expression processing +Patch116: libssh-CVE-2026-0967-match-Avoid-recursive-matching-ReDoS.patch +# PATCH-FIX-UPSTREAM CVE-2026-0968 bsc#1258080 Denial of Service due to malformed SFTP message +Patch117: libssh-CVE-2026-0968-sftp-Sanitize-input-handling-in-sftp_p.patch BuildRequires: cmake BuildRequires: gcc-c++ BuildRequires: krb5-devel