Sync from SUSE:ALP:Source:Standard:1.0 libssh revision 984bfeba8c95feb47bb2867501ef34e5
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
353
libssh-CVE-2026-0967-match-Avoid-recursive-matching-ReDoS.patch
Normal file
353
libssh-CVE-2026-0967-match-Avoid-recursive-matching-ReDoS.patch
Normal 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
|
||||
|
||||
@@ -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++;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
12
libssh.spec
12
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
|
||||
|
||||
Reference in New Issue
Block a user