diff --git a/0031-bugfix-for-CVE-2026-0964.patch b/0031-bugfix-for-CVE-2026-0964.patch new file mode 100644 index 0000000000000000000000000000000000000000..e8cdeb22541d9420ca0d6ffa1925f0bc5bde4945 --- /dev/null +++ b/0031-bugfix-for-CVE-2026-0964.patch @@ -0,0 +1,42 @@ +From a5e4b12090b0c939d85af4f29280e40c5b6600aa Mon Sep 17 00:00:00 2001 +From: Jakub Jelen +Date: Mon, 22 Dec 2025 19:16:44 +0100 +Subject: CVE-2026-0964 scp: Reject invalid paths received through scp + +Signed-off-by: Jakub Jelen +Reviewed-by: Andreas Schneider +(cherry picked from commit daa80818f89347b4d80b0c5b80659f9a9e55e8cc) +--- + src/scp.c | 16 ++++++++++++++++ + 1 file changed, 16 insertions(+) + +diff --git a/src/scp.c b/src/scp.c +index 103822c..669a71a 100644 +--- a/src/scp.c ++++ b/src/scp.c +@@ -848,6 +848,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.43.5 + diff --git a/0032-bugfix-for-CVE-2026-0966.patch b/0032-bugfix-for-CVE-2026-0966.patch new file mode 100644 index 0000000000000000000000000000000000000000..3a492846be4be40292e8f155326d120a35beb2e7 --- /dev/null +++ b/0032-bugfix-for-CVE-2026-0966.patch @@ -0,0 +1,31 @@ +From 6ba5ff1b7b1547a59f750fbc06b89737b7456117 Mon Sep 17 00:00:00 2001 +From: Jakub Jelen +Date: Thu, 8 Jan 2026 12:09:50 +0100 +Subject: 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) +--- + src/misc.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/src/misc.c b/src/misc.c +index f371f33..565abcf 100644 +--- a/src/misc.c ++++ b/src/misc.c +@@ -451,7 +451,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.43.5 + diff --git a/0033-bugfix-for-CVE-2026-0967.patch b/0033-bugfix-for-CVE-2026-0967.patch new file mode 100644 index 0000000000000000000000000000000000000000..a36ed01ade5d8fb19b04a643b5c2e79d3dcb562a --- /dev/null +++ b/0033-bugfix-for-CVE-2026-0967.patch @@ -0,0 +1,362 @@ +From 6d74aa6138895b3662bade9bd578338b0c4f8a15 Mon Sep 17 00:00:00 2001 +From: Jakub Jelen +Date: Wed, 17 Dec 2025 18:48:34 +0100 +Subject: 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) + +--- + src/match.c | 120 ++++++++++++++----------------- + tests/unittests/torture_config.c | 116 ++++++++++++++++++++++-------- + 2 files changed, 139 insertions(+), 97 deletions(-) + +diff --git a/src/match.c b/src/match.c +index 3e58f73..e94e7c7 100644 +--- a/src/match.c ++++ b/src/match.c +@@ -43,85 +43,69 @@ + + #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; +- } +- pattern++; +- } +- +- 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. +- */ +- 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; ++ while (*s) { ++ /* Case 1: Exact match or '?' wildcard */ ++ if (*pattern == *s || *pattern == '?') { ++ s++; ++ pattern++; ++ continue; ++ } ++ /* Case 2: '*' wildcard */ ++ if (*pattern == '*') { ++ /* Record the position of the star and the current string position. ++ * We optimistically assume * matches 0 characters first. ++ */ ++ p_star = ++pattern; ++ s_star = s; ++ continue; ++ } ++ ++ /* 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; + } + +- /* Check if the next character of the string is acceptable. */ +- if (*pattern != '?' && *pattern != *s) { +- return 0; +- } ++ /* Case 4: Mismatch and no star to backtrack to */ ++ return 0; + +- /* Move to the next character, both in string and in pattern. */ +- s++; ++ } ++ /* 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'); + } + + /* +@@ -172,7 +156,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 26a2421..9e53057 100644 +--- a/tests/unittests/torture_config.c ++++ b/tests/unittests/torture_config.c +@@ -1656,80 +1656,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.43.7 + diff --git a/0034-bugfix-for-CVE-2026-0968.patch b/0034-bugfix-for-CVE-2026-0968.patch new file mode 100644 index 0000000000000000000000000000000000000000..0b98c94b0541b9f191a82d2251848dc4775a9359 --- /dev/null +++ b/0034-bugfix-for-CVE-2026-0968.patch @@ -0,0 +1,56 @@ +From 796d85f786dff62bd4bcc4408d9b7bbc855841e9 Mon Sep 17 00:00:00 2001 +From: Jakub Jelen +Date: Mon, 22 Dec 2025 20:59:11 +0100 +Subject: 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) + +--- + src/sftp.c | 16 +++++++++++++--- + 1 file changed, 13 insertions(+), 3 deletions(-) + +diff --git a/src/sftp.c b/src/sftp.c +index e01012a..6722a9d 100644 +--- a/src/sftp.c ++++ b/src/sftp.c +@@ -1289,13 +1289,18 @@ static char *sftp_parse_longname(const char *longname, + const char *p, *q; + 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 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++; + } + +-- +2.43.5 + diff --git a/libssh.spec b/libssh.spec index fbd5f2801a5e7ce101e0ab4cb877f4594128f8b1..52f6faf93c76138d5e14e7d7f0f1ca2cce14b5c2 100644 --- a/libssh.spec +++ b/libssh.spec @@ -1,4 +1,4 @@ -%define anolis_release 12 +%define anolis_release 13 %global _smp_build_ncpus 1 Name: libssh @@ -59,6 +59,14 @@ Patch0031: 0030-bugfix-for-CVE-2025-8277-2.patch Patch0032: 0030-bugfix-for-CVE-2025-8277-3.patch # https://git.libssh.org/projects/libssh.git/commit/?id=ffed80f8c078122990a4eba2b275facd56dd43e0 Patch0033: 0030-bugfix-for-CVE-2025-8277-4.patch +# https://git.libssh.org/projects/libssh.git/commit/?id=a5e4b12090b0c939d85af4f29280e40c5b6600aa +Patch0034: 0031-bugfix-for-CVE-2026-0964.patch +# https://git.libssh.org/projects/libssh.git/commit/?id=6ba5ff1b7b1547a59f750fbc06b89737b7456117 +Patch0035: 0032-bugfix-for-CVE-2026-0966.patch +# https://git.libssh.org/projects/libssh.git/commit/?id=6d74aa6138895b3662bade9bd578338b0c4f8a15 +Patch0036: 0033-bugfix-for-CVE-2026-0967.patch +# https://git.libssh.org/projects/libssh.git/commit/?id=796d85f786dff62bd4bcc4408d9b7bbc855841e9 +Patch0037: 0034-bugfix-for-CVE-2026-0968.patch BuildRequires: cmake gcc-c++ BuildRequires: openssl-devel zlib-devel krb5-devel libcmocka-devel @@ -169,6 +177,9 @@ popd %doc AUTHORS CHANGELOG README %changelog +* Thu Feb 12 2026 lzq11122 - 0.10.5-13 +- Add patch to fix CVE-2026-0964,CVE-2026-0966,CVE-2026-0967,CVE-2026-0968 + * Thu Nov 27 2025 YangCheng - 0.10.5-12 - Add patch to fix CVE-2025-8277