diff --git a/debian/changelog b/debian/changelog index 9eba1e4..0edc6e7 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,27 @@ +libssh (0.11.3-1deepin5) unstable; urgency=medium + + * fix(cve): CVE-2026-0965 + + -- deepin-ci-robot Tue, 21 Jul 2026 10:21:34 +0800 + +libssh (0.11.3-1deepin4) unstable; urgency=medium + + * fix(cve): CVE-2026-0968 + + -- deepin-ci-robot Tue, 21 Jul 2026 10:14:36 +0800 + +libssh (0.11.3-1deepin3) unstable; urgency=medium + + * fix(cve): CVE-2026-0964 + + -- deepin-ci-robot Tue, 21 Jul 2026 10:08:08 +0800 + +libssh (0.11.3-1deepin2) unstable; urgency=medium + + * fix(cve): CVE-2026-0967 + + -- deepin-ci-robot Sat, 18 Jul 2026 08:44:40 +0800 + libssh (0.11.3-1deepin1) unstable; urgency=medium * Fix CVE-2026-3731: out-of-bounds read in sftp_extensions_get_name diff --git a/debian/patches/CVE-2026-0964.patch b/debian/patches/CVE-2026-0964.patch new file mode 100644 index 0000000..1e84ae2 --- /dev/null +++ b/debian/patches/CVE-2026-0964.patch @@ -0,0 +1,34 @@ +Description: CVE-2026-0964 - 安全修复 +Author: Jakub Jelen +Origin: https://gitlab.com/libssh/libssh-mirror/commit/a5e4b12090b0c939d85af4f29280e40c5b6600aa +Bug: https://nvd.nist.gov/vuln/detail/CVE-2026-0964 +Last-Update: Mon, 22 Dec 2025 19:16:44 +0100 +--- + +diff --git a/src/scp.c b/src/scp.c +index a1e3687..08a29fa 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') { diff --git a/debian/patches/CVE-2026-0965.patch b/debian/patches/CVE-2026-0965.patch new file mode 100644 index 0000000..6434ea1 --- /dev/null +++ b/debian/patches/CVE-2026-0965.patch @@ -0,0 +1,262 @@ +Description: CVE-2026-0965 - 安全修复 +Author: Jakub Jelen +Origin: https://git.libssh.org/projects/libssh/commit/bf390a042623e02abc8f421c4c5fadc0429a8a76 +Bug: https://nvd.nist.gov/vuln/detail/CVE-2026-0965 +Last-Update: 2025-12-11 +--- + +diff --git a/include/libssh/misc.h b/include/libssh/misc.h +index ab726a0..8eab94e 100644 +--- a/include/libssh/misc.h ++++ b/include/libssh/misc.h +@@ -36,6 +36,7 @@ + #include + #include + #endif /* _WIN32 */ ++#include + + #ifdef __cplusplus + extern "C" { +@@ -136,6 +137,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 35fd850..6206997 100644 +--- a/include/libssh/priv.h ++++ b/include/libssh/priv.h +@@ -473,6 +473,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 9e4a7fd..c12f100 100644 +--- a/src/bind_config.c ++++ b/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 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 b4171ef..1ffad53 100644 +--- a/src/config.c ++++ b/src/config.c +@@ -223,10 +223,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; + } + +@@ -1466,8 +1465,9 @@ int ssh_config_parse_file(ssh_session session, const char *filename) + 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; + } + +diff --git a/src/dh-gex.c b/src/dh-gex.c +index 46ba934..428a565 100644 +--- a/src/dh-gex.c ++++ b/src/dh-gex.c +@@ -519,9 +519,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 3ef83e2..701576c 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 a2d08a7..3ab468d 100644 +--- a/src/knownhosts.c ++++ b/src/knownhosts.c +@@ -232,7 +232,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 774211f..3968e6b 100644 +--- a/src/misc.c ++++ b/src/misc.c +@@ -37,6 +37,7 @@ + #endif /* _WIN32 */ + + #include ++#include + #include + #include + #include +@@ -2244,4 +2245,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/tests/unittests/torture_config.c b/tests/unittests/torture_config.c +index 9854daa..f5250b2 100644 +--- a/tests/unittests/torture_config.c ++++ b/tests/unittests/torture_config.c +@@ -2675,6 +2675,23 @@ static void torture_config_match_complex(void **state) + ssh_string_free_char(v); + } + ++/* 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; +@@ -2771,6 +2788,9 @@ int torture_run_tests(void) + setup, teardown), + cmocka_unit_test_setup_teardown(torture_config_match_complex, + setup, teardown), ++ cmocka_unit_test_setup_teardown(torture_config_invalid, ++ setup, ++ teardown), + }; + + diff --git a/debian/patches/CVE-2026-0967.patch b/debian/patches/CVE-2026-0967.patch new file mode 100644 index 0000000..b5e23ab --- /dev/null +++ b/debian/patches/CVE-2026-0967.patch @@ -0,0 +1,325 @@ +Description: CVE-2026-0967 - 安全修复 +Author: Jakub Jelen +Origin: https://gitlab.com/libssh/libssh-mirror.git/commit/6d74aa6138895b3662bade9bd578338b0c4f8a15 +Bug: https://nvd.nist.gov/vuln/detail/CVE-2026-0967 +Last-Update: 2025-12-17 +--- + +diff --git a/src/match.c b/src/match.c +index 2c004c9..771ee63 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 e9706d5..9854daa 100644 +--- a/tests/unittests/torture_config.c ++++ b/tests/unittests/torture_config.c +@@ -2342,80 +2342,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 diff --git a/debian/patches/CVE-2026-0968.patch b/debian/patches/CVE-2026-0968.patch new file mode 100644 index 0000000..4b1f31a --- /dev/null +++ b/debian/patches/CVE-2026-0968.patch @@ -0,0 +1,54 @@ +Description: CVE-2026-0968 - 安全修复 +Author: Jakub Jelen +Origin: https://gitlab.com/libssh/libssh-mirror/commit/20856f44c146468c830da61dcbbbaa8ce71e390b +Bug: https://nvd.nist.gov/vuln/detail/CVE-2026-0968 +Last-Update: 2025-12-22 20:59:11 +0100 +--- + +diff --git a/src/sftp_common.c b/src/sftp_common.c +index 13512b8..d6189b7 100644 +--- a/src/sftp_common.c ++++ b/src/sftp_common.c +@@ -458,19 +458,24 @@ 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 { +@@ -478,8 +483,13 @@ static char * sftp_parse_longname(const char *longname, + } + } + ++ /* 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/debian/patches/series b/debian/patches/series index 88bad1f..7a34d0c 100644 --- a/debian/patches/series +++ b/debian/patches/series @@ -2,3 +2,7 @@ 2003-disable-expand_tilde_unix-test.patch 2004-install-static-lib.patch 2005-fix-CVE-2026-3731.patch +CVE-2026-0967.patch +CVE-2026-0964.patch +CVE-2026-0968.patch +CVE-2026-0965.patch