Sync from SUSE:ALP:Source:Standard:1.0 libssh revision 984bfeba8c95feb47bb2867501ef34e5

This commit is contained in:
2026-02-26 11:33:57 +01:00
parent 11e006dd3e
commit c3ddec026a
9 changed files with 894 additions and 0 deletions

View File

@@ -0,0 +1,40 @@
From a5e4b12090b0c939d85af4f29280e40c5b6600aa Mon Sep 17 00:00:00 2001
From: Jakub Jelen <jjelen@redhat.com>
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 <jjelen@redhat.com>
Reviewed-by: Andreas Schneider <asn@cryptomilk.org>
(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 ? "<NULL>" : name);
+ SAFE_FREE(name);
+ goto error;
+ }
SAFE_FREE(scp->request_name);
scp->request_name = name;
if (buffer[0] == 'C') {
--
2.52.0

View File

@@ -0,0 +1,267 @@
From bf390a042623e02abc8f421c4c5fadc0429a8a76 Mon Sep 17 00:00:00 2001
From: Jakub Jelen <jjelen@redhat.com>
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 <jjelen@redhat.com>
Reviewed-by: Andreas Schneider <asn@cryptomilk.org>
(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 <stdio.h>
+
#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 <errno.h>
+#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <string.h>
@@ -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),
};

View File

@@ -0,0 +1,59 @@
From 3e1d276a5a030938a8f144f46ff4f2a2efe31ced Mon Sep 17 00:00:00 2001
From: Jakub Jelen <jjelen@redhat.com>
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 <jjelen@redhat.com>
Reviewed-by: Pavol Žáčik <pzacik@redhat.com>
(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

View File

@@ -0,0 +1,29 @@
From 6ba5ff1b7b1547a59f750fbc06b89737b7456117 Mon Sep 17 00:00:00 2001
From: Jakub Jelen <jjelen@redhat.com>
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 <jjelen@redhat.com>
Reviewed-by: Pavol Žáčik <pzacik@redhat.com>
(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

View File

@@ -0,0 +1,61 @@
From b156391833c66322436cf177d57e10b0325fbcc8 Mon Sep 17 00:00:00 2001
From: Jakub Jelen <jjelen@redhat.com>
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 <jjelen@redhat.com>
Reviewed-by: Pavol Žáčik <pzacik@redhat.com>
(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();

View File

@@ -0,0 +1,353 @@
From 6d74aa6138895b3662bade9bd578338b0c4f8a15 Mon Sep 17 00:00:00 2001
From: Jakub Jelen <jjelen@redhat.com>
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 <jjelen@redhat.com>
Reviewed-by: Pavol Žáčik <pzacik@redhat.com>
(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

View File

@@ -0,0 +1,54 @@
From 796d85f786dff62bd4bcc4408d9b7bbc855841e9 Mon Sep 17 00:00:00 2001
From: Jakub Jelen <jjelen@redhat.com>
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 <jjelen@redhat.com>
Reviewed-by: Andreas Schneider <asn@cryptomilk.org>
(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++;
}

View File

@@ -1,3 +1,22 @@
-------------------------------------------------------------------
Wed Feb 11 11:28:10 UTC 2026 - Pedro Monreal <pmonreal@suse.com>
- 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 <pmonreal@suse.com>

View File

@@ -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