Backport low/medium CVE patches
Resolves: RHEL-150661 Resolves: CVE-2025-4877 Resolves: CVE-2025-4878 Resolves: CVE-2025-5351 Resolves: CVE-2025-8114 Resolves: CVE-2025-8277 Resolves: CVE-2026-0964 Resolves: CVE-2026-0965 Resolves: CVE-2026-0966 Resolves: CVE-2026-0967 Resolves: CVE-2026-0968
This commit is contained in:
parent
b903d9f035
commit
048ffad847
54
CVE-2025-4877.patch
Normal file
54
CVE-2025-4877.patch
Normal file
@ -0,0 +1,54 @@
|
||||
From c9d785835f5b299c31b22be0506a658205c169bf Mon Sep 17 00:00:00 2001
|
||||
From: Jakub Jelen <jjelen@redhat.com>
|
||||
Date: Tue, 15 Apr 2025 11:41:24 +0200
|
||||
Subject: [PATCH] CVE-2025-4877 base64: Prevent integer overflow and potential
|
||||
OOB
|
||||
|
||||
Set maximum input to 256MB to have safe margin to the 1GB trigger point
|
||||
for 32b arch.
|
||||
|
||||
The OOB should not be reachable by any internal code paths as most of
|
||||
the buffers and strings we use as input for this operation already have
|
||||
similar limit and none really allows this much of data.
|
||||
|
||||
Signed-off-by: Jakub Jelen <jjelen@redhat.com>
|
||||
Reviewed-by: Andreas Schneider <asn@cryptomilk.org>
|
||||
(cherry picked from commit 00f09acbec55962839fc7837ef14c56fb8fbaf72)
|
||||
---
|
||||
src/base64.c | 13 ++++++++++++-
|
||||
1 file changed, 12 insertions(+), 1 deletion(-)
|
||||
|
||||
diff --git a/src/base64.c b/src/base64.c
|
||||
index 4148f49c..f42e0e80 100644
|
||||
--- a/src/base64.c
|
||||
+++ b/src/base64.c
|
||||
@@ -29,6 +29,9 @@
|
||||
#include "libssh/priv.h"
|
||||
#include "libssh/buffer.h"
|
||||
|
||||
+/* Do not allow encoding more than 256MB of data */
|
||||
+#define BASE64_MAX_INPUT_LEN 256 * 1024 * 1024
|
||||
+
|
||||
static
|
||||
const uint8_t alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
"abcdefghijklmnopqrstuvwxyz"
|
||||
@@ -274,7 +277,15 @@ uint8_t *bin_to_base64(const uint8_t *source, size_t len)
|
||||
{
|
||||
uint8_t *base64 = NULL;
|
||||
uint8_t *ptr = NULL;
|
||||
- size_t flen = len + (3 - (len % 3)); /* round to upper 3 multiple */
|
||||
+ size_t flen = 0;
|
||||
+
|
||||
+ /* Set the artificial upper limit for the input. Otherwise on 32b arch, the
|
||||
+ * following line could overflow for sizes larger than SIZE_MAX / 4 */
|
||||
+ if (len > BASE64_MAX_INPUT_LEN) {
|
||||
+ return NULL;
|
||||
+ }
|
||||
+
|
||||
+ flen = len + (3 - (len % 3)); /* round to upper 3 multiple */
|
||||
flen = (4 * flen) / 3 + 1;
|
||||
|
||||
base64 = malloc(flen);
|
||||
--
|
||||
2.53.0
|
||||
|
||||
2630
CVE-2025-4878.patch
Normal file
2630
CVE-2025-4878.patch
Normal file
File diff suppressed because it is too large
Load Diff
27
CVE-2025-5351.patch
Normal file
27
CVE-2025-5351.patch
Normal file
@ -0,0 +1,27 @@
|
||||
From f9cbbd9359dc17a0388d7b7731546dedb0c34de5 Mon Sep 17 00:00:00 2001
|
||||
From: Jakub Jelen <jjelen@redhat.com>
|
||||
Date: Tue, 6 May 2025 22:43:31 +0200
|
||||
Subject: [PATCH] CVE-2025-5351 pki_crypto: Avoid double-free on low-memory
|
||||
conditions
|
||||
|
||||
Signed-off-by: Jakub Jelen <jjelen@redhat.com>
|
||||
Reviewed-by: Andreas Schneider <asn@cryptomilk.org>
|
||||
---
|
||||
src/pki_crypto.c | 1 +
|
||||
1 file changed, 1 insertion(+)
|
||||
|
||||
diff --git a/src/pki_crypto.c b/src/pki_crypto.c
|
||||
index b99a6e8c..bde19238 100644
|
||||
--- a/src/pki_crypto.c
|
||||
+++ b/src/pki_crypto.c
|
||||
@@ -2023,6 +2023,7 @@ ssh_string pki_publickey_to_blob(const ssh_key key)
|
||||
bignum_safe_free(bn);
|
||||
bignum_safe_free(be);
|
||||
OSSL_PARAM_free(params);
|
||||
+ params = NULL;
|
||||
#endif /* OPENSSL_VERSION_NUMBER */
|
||||
break;
|
||||
}
|
||||
--
|
||||
2.53.0
|
||||
|
||||
45
CVE-2025-8114.patch
Normal file
45
CVE-2025-8114.patch
Normal file
@ -0,0 +1,45 @@
|
||||
From 7a76f6e268e4293e64358f65d1c253de5c40f875 Mon Sep 17 00:00:00 2001
|
||||
From: Andreas Schneider <asn@cryptomilk.org>
|
||||
Date: Wed, 6 Aug 2025 15:17:59 +0200
|
||||
Subject: [PATCH] CVE-2025-8114: Fix NULL pointer dereference after allocation
|
||||
failure
|
||||
|
||||
Signed-off-by: Andreas Schneider <asn@cryptomilk.org>
|
||||
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
|
||||
(cherry picked from commit 53ac23ded4cb2c5463f6c4cd1525331bd578812d)
|
||||
---
|
||||
src/kex.c | 4 ++++
|
||||
1 file changed, 4 insertions(+)
|
||||
|
||||
diff --git a/src/kex.c b/src/kex.c
|
||||
index f71482fd..64aacb9e 100644
|
||||
--- a/src/kex.c
|
||||
+++ b/src/kex.c
|
||||
@@ -1494,6 +1494,8 @@ int ssh_make_sessionid(ssh_session session)
|
||||
ssh_log_hexdump("hash buffer", ssh_buffer_get(buf), ssh_buffer_get_len(buf));
|
||||
#endif
|
||||
|
||||
+ /* Set rc for the following switch statement in case we goto error. */
|
||||
+ rc = SSH_ERROR;
|
||||
switch (session->next_crypto->kex_type) {
|
||||
case SSH_KEX_DH_GROUP1_SHA1:
|
||||
case SSH_KEX_DH_GROUP14_SHA1:
|
||||
@@ -1553,6 +1555,7 @@ int ssh_make_sessionid(ssh_session session)
|
||||
session->next_crypto->secret_hash);
|
||||
break;
|
||||
}
|
||||
+
|
||||
/* During the first kex, secret hash and session ID are equal. However, after
|
||||
* a key re-exchange, a new secret hash is calculated. This hash will not replace
|
||||
* but complement existing session id.
|
||||
@@ -1561,6 +1564,7 @@ int ssh_make_sessionid(ssh_session session)
|
||||
session->next_crypto->session_id = malloc(session->next_crypto->digest_len);
|
||||
if (session->next_crypto->session_id == NULL) {
|
||||
ssh_set_error_oom(session);
|
||||
+ rc = SSH_ERROR;
|
||||
goto error;
|
||||
}
|
||||
memcpy(session->next_crypto->session_id, session->next_crypto->secret_hash,
|
||||
--
|
||||
2.53.0
|
||||
|
||||
184
CVE-2025-8277.patch
Normal file
184
CVE-2025-8277.patch
Normal file
@ -0,0 +1,184 @@
|
||||
From a55ed6b67033dace401dc42c312d7b3c0ec740a3 Mon Sep 17 00:00:00 2001
|
||||
From: Jakub Jelen <jjelen@redhat.com>
|
||||
Date: Tue, 5 Aug 2025 18:42:31 +0200
|
||||
Subject: [PATCH 1/3] CVE-2025-8277: packet: Adjust packet filter to work when
|
||||
DH-GEX is guessed wrongly
|
||||
|
||||
Signed-off-by: Jakub Jelen <jjelen@redhat.com>
|
||||
Reviewed-by: Andreas Schneider <asn@cryptomilk.org>
|
||||
(cherry picked from commit 4310a696f2d632c6742678077d703d9b9ff3bc0e)
|
||||
---
|
||||
src/packet.c | 2 ++
|
||||
1 file changed, 2 insertions(+)
|
||||
|
||||
diff --git a/src/packet.c b/src/packet.c
|
||||
index 891d120c..af9a55da 100644
|
||||
--- a/src/packet.c
|
||||
+++ b/src/packet.c
|
||||
@@ -294,6 +294,7 @@ static enum ssh_packet_filter_result_e ssh_packet_incoming_filter(ssh_session se
|
||||
* or session_state == SSH_SESSION_STATE_INITIAL_KEX
|
||||
* - dh_handshake_state == DH_STATE_INIT
|
||||
* or dh_handshake_state == DH_STATE_INIT_SENT (re-exchange)
|
||||
+ * or dh_handshake_state == DH_STATE_REQUEST_SENT (dh-gex)
|
||||
* or dh_handshake_state == DH_STATE_FINISHED (re-exchange)
|
||||
*
|
||||
* Transitions:
|
||||
@@ -313,6 +314,7 @@ static enum ssh_packet_filter_result_e ssh_packet_incoming_filter(ssh_session se
|
||||
|
||||
if ((session->dh_handshake_state != DH_STATE_INIT) &&
|
||||
(session->dh_handshake_state != DH_STATE_INIT_SENT) &&
|
||||
+ (session->dh_handshake_state != DH_STATE_REQUEST_SENT) &&
|
||||
(session->dh_handshake_state != DH_STATE_FINISHED))
|
||||
{
|
||||
rc = SSH_PACKET_DENIED;
|
||||
--
|
||||
2.53.0
|
||||
|
||||
|
||||
From 00bdcdc96ca6e367a2a2dd05ff56ca9e6e516f45 Mon Sep 17 00:00:00 2001
|
||||
From: Francesco Rollo <eferollo@gmail.com>
|
||||
Date: Thu, 24 Jul 2025 16:30:07 +0300
|
||||
Subject: [PATCH 2/3] CVE-2025-8277: Fix memory leak of unused ephemeral key
|
||||
pair after client's wrong KEX guess
|
||||
|
||||
Signed-off-by: Francesco Rollo <eferollo@gmail.com>
|
||||
Reviewed-by: Andreas Schneider <asn@cryptomilk.org>
|
||||
(cherry picked from commit ccff22d3787c1355b3f0dcd09fe54d90acc55bf1)
|
||||
---
|
||||
src/dh_crypto.c | 5 +++++
|
||||
src/dh_key.c | 5 +++++
|
||||
src/ecdh_crypto.c | 5 +++++
|
||||
src/ecdh_gcrypt.c | 6 ++++++
|
||||
src/ecdh_mbedcrypto.c | 6 ++++++
|
||||
5 files changed, 27 insertions(+)
|
||||
|
||||
diff --git a/src/dh_crypto.c b/src/dh_crypto.c
|
||||
index 85249bfc..60f35912 100644
|
||||
--- a/src/dh_crypto.c
|
||||
+++ b/src/dh_crypto.c
|
||||
@@ -407,6 +407,11 @@ int ssh_dh_init_common(struct ssh_crypto_struct *crypto)
|
||||
struct dh_ctx *ctx = NULL;
|
||||
int rc;
|
||||
|
||||
+ /* Cleanup any previously allocated dh_ctx */
|
||||
+ if (crypto->dh_ctx != NULL) {
|
||||
+ ssh_dh_cleanup(crypto);
|
||||
+ }
|
||||
+
|
||||
ctx = calloc(1, sizeof(*ctx));
|
||||
if (ctx == NULL) {
|
||||
return SSH_ERROR;
|
||||
diff --git a/src/dh_key.c b/src/dh_key.c
|
||||
index bda54b17..637b1b44 100644
|
||||
--- a/src/dh_key.c
|
||||
+++ b/src/dh_key.c
|
||||
@@ -237,6 +237,11 @@ int ssh_dh_init_common(struct ssh_crypto_struct *crypto)
|
||||
struct dh_ctx *ctx = NULL;
|
||||
int rc;
|
||||
|
||||
+ /* Cleanup any previously allocated dh_ctx */
|
||||
+ if (crypto->dh_ctx != NULL) {
|
||||
+ ssh_dh_cleanup(crypto);
|
||||
+ }
|
||||
+
|
||||
ctx = calloc(1, sizeof(*ctx));
|
||||
if (ctx == NULL) {
|
||||
return SSH_ERROR;
|
||||
diff --git a/src/ecdh_crypto.c b/src/ecdh_crypto.c
|
||||
index 178d9060..c61b6ce4 100644
|
||||
--- a/src/ecdh_crypto.c
|
||||
+++ b/src/ecdh_crypto.c
|
||||
@@ -219,6 +219,11 @@ int ssh_client_ecdh_init(ssh_session session){
|
||||
return SSH_ERROR;
|
||||
}
|
||||
|
||||
+ if (session->next_crypto->ecdh_privkey != NULL) {
|
||||
+ EC_KEY_free(session->next_crypto->ecdh_privkey);
|
||||
+ session->next_crypto->ecdh_privkey = NULL;
|
||||
+ }
|
||||
+
|
||||
session->next_crypto->ecdh_privkey = key;
|
||||
session->next_crypto->ecdh_client_pubkey = client_pubkey;
|
||||
|
||||
diff --git a/src/ecdh_gcrypt.c b/src/ecdh_gcrypt.c
|
||||
index 722428c2..f49e8eb2 100644
|
||||
--- a/src/ecdh_gcrypt.c
|
||||
+++ b/src/ecdh_gcrypt.c
|
||||
@@ -101,6 +101,12 @@ int ssh_client_ecdh_init(ssh_session session)
|
||||
goto out;
|
||||
}
|
||||
|
||||
+ /* Free any previously allocated privkey */
|
||||
+ if (session->next_crypto->ecdh_privkey != NULL) {
|
||||
+ gcry_sexp_release(session->next_crypto->ecdh_privkey);
|
||||
+ session->next_crypto->ecdh_privkey = NULL;
|
||||
+ }
|
||||
+
|
||||
session->next_crypto->ecdh_privkey = key;
|
||||
key = NULL;
|
||||
session->next_crypto->ecdh_client_pubkey = client_pubkey;
|
||||
diff --git a/src/ecdh_mbedcrypto.c b/src/ecdh_mbedcrypto.c
|
||||
index 1d9c8f36..d31bfcc7 100644
|
||||
--- a/src/ecdh_mbedcrypto.c
|
||||
+++ b/src/ecdh_mbedcrypto.c
|
||||
@@ -70,6 +70,12 @@ int ssh_client_ecdh_init(ssh_session session)
|
||||
return SSH_ERROR;
|
||||
}
|
||||
|
||||
+ /* Free any previously allocated privkey */
|
||||
+ if (session->next_crypto->ecdh_privkey != NULL) {
|
||||
+ mbedtls_ecp_keypair_free(session->next_crypto->ecdh_privkey);
|
||||
+ SAFE_FREE(session->next_crypto->ecdh_privkey);
|
||||
+ }
|
||||
+
|
||||
session->next_crypto->ecdh_privkey = malloc(sizeof(mbedtls_ecp_keypair));
|
||||
if (session->next_crypto->ecdh_privkey == NULL) {
|
||||
return SSH_ERROR;
|
||||
--
|
||||
2.53.0
|
||||
|
||||
|
||||
From 2bdddb2bd34cdd563de9227d74c5a21784c03d3d Mon Sep 17 00:00:00 2001
|
||||
From: Jakub Jelen <jjelen@redhat.com>
|
||||
Date: Wed, 6 Aug 2025 11:10:38 +0200
|
||||
Subject: [PATCH 3/3] CVE-2025-8277: ecdh: Free previously allocated pubkeys
|
||||
|
||||
Signed-off-by: Jakub Jelen <jjelen@redhat.com>
|
||||
Reviewed-by: Andreas Schneider <asn@cryptomilk.org>
|
||||
(cherry picked from commit c9d95ab0c7a52b231bcec09afbea71944ed0d852)
|
||||
---
|
||||
src/ecdh_crypto.c | 1 +
|
||||
src/ecdh_gcrypt.c | 3 ++-
|
||||
2 files changed, 3 insertions(+), 1 deletion(-)
|
||||
|
||||
diff --git a/src/ecdh_crypto.c b/src/ecdh_crypto.c
|
||||
index c61b6ce4..5694b083 100644
|
||||
--- a/src/ecdh_crypto.c
|
||||
+++ b/src/ecdh_crypto.c
|
||||
@@ -223,6 +223,7 @@ int ssh_client_ecdh_init(ssh_session session){
|
||||
EC_KEY_free(session->next_crypto->ecdh_privkey);
|
||||
session->next_crypto->ecdh_privkey = NULL;
|
||||
}
|
||||
+ SSH_STRING_FREE(session->next_crypto->ecdh_client_pubkey);
|
||||
|
||||
session->next_crypto->ecdh_privkey = key;
|
||||
session->next_crypto->ecdh_client_pubkey = client_pubkey;
|
||||
diff --git a/src/ecdh_gcrypt.c b/src/ecdh_gcrypt.c
|
||||
index f49e8eb2..ee5350cd 100644
|
||||
--- a/src/ecdh_gcrypt.c
|
||||
+++ b/src/ecdh_gcrypt.c
|
||||
@@ -106,9 +106,10 @@ int ssh_client_ecdh_init(ssh_session session)
|
||||
gcry_sexp_release(session->next_crypto->ecdh_privkey);
|
||||
session->next_crypto->ecdh_privkey = NULL;
|
||||
}
|
||||
-
|
||||
session->next_crypto->ecdh_privkey = key;
|
||||
key = NULL;
|
||||
+
|
||||
+ SSH_STRING_FREE(session->next_crypto->ecdh_client_pubkey);
|
||||
session->next_crypto->ecdh_client_pubkey = client_pubkey;
|
||||
client_pubkey = NULL;
|
||||
|
||||
--
|
||||
2.53.0
|
||||
|
||||
42
CVE-2026-0964.patch
Normal file
42
CVE-2026-0964.patch
Normal file
@ -0,0 +1,42 @@
|
||||
From 5be2d4950b4030da2f95299e12905ea9e51dbedf Mon Sep 17 00:00:00 2001
|
||||
From: Jakub Jelen <jjelen@redhat.com>
|
||||
Date: Mon, 22 Dec 2025 19:16:44 +0100
|
||||
Subject: [PATCH] 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)
|
||||
---
|
||||
src/scp.c | 16 ++++++++++++++++
|
||||
1 file changed, 16 insertions(+)
|
||||
|
||||
diff --git a/src/scp.c b/src/scp.c
|
||||
index 04eb4f1b..a33af16b 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 ? "<NULL>" : name);
|
||||
+ SAFE_FREE(name);
|
||||
+ goto error;
|
||||
+ }
|
||||
SAFE_FREE(scp->request_name);
|
||||
scp->request_name = name;
|
||||
if (buffer[0] == 'C') {
|
||||
--
|
||||
2.53.0
|
||||
|
||||
273
CVE-2026-0965.patch
Normal file
273
CVE-2026-0965.patch
Normal file
@ -0,0 +1,273 @@
|
||||
From 63079a26261b5ea6736fcdf68077135e7f2c991d Mon Sep 17 00:00:00 2001
|
||||
From: Jakub Jelen <jjelen@redhat.com>
|
||||
Date: Thu, 11 Dec 2025 17:33:19 +0100
|
||||
Subject: [PATCH] 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)
|
||||
---
|
||||
include/libssh/misc.h | 4 ++
|
||||
include/libssh/priv.h | 3 ++
|
||||
src/bind_config.c | 4 +-
|
||||
src/config.c | 5 ++-
|
||||
src/dh-gex.c | 4 +-
|
||||
src/known_hosts.c | 2 +-
|
||||
src/knownhosts.c | 2 +-
|
||||
src/misc.c | 74 ++++++++++++++++++++++++++++++++
|
||||
tests/unittests/torture_config.c | 19 ++++++++
|
||||
9 files changed, 109 insertions(+), 8 deletions(-)
|
||||
|
||||
diff --git a/include/libssh/misc.h b/include/libssh/misc.h
|
||||
index 47a423f2..3452251d 100644
|
||||
--- a/include/libssh/misc.h
|
||||
+++ b/include/libssh/misc.h
|
||||
@@ -21,6 +21,8 @@
|
||||
#ifndef MISC_H_
|
||||
#define MISC_H_
|
||||
|
||||
+#include <stdio.h>
|
||||
+
|
||||
/* in misc.c */
|
||||
/* gets the user home dir. */
|
||||
char *ssh_get_user_home_dir(void);
|
||||
@@ -102,4 +104,6 @@ char *ssh_strreplace(const char *src, const char *pattern, const char *repl);
|
||||
|
||||
int ssh_check_hostname_syntax(const char *hostname);
|
||||
|
||||
+FILE *ssh_strict_fopen(const char *filename, size_t max_file_size);
|
||||
+
|
||||
#endif /* MISC_H_ */
|
||||
diff --git a/include/libssh/priv.h b/include/libssh/priv.h
|
||||
index bab761b0..876ea4ed 100644
|
||||
--- a/include/libssh/priv.h
|
||||
+++ b/include/libssh/priv.h
|
||||
@@ -434,4 +434,7 @@ 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
|
||||
+
|
||||
#endif /* _LIBSSH_PRIV_H */
|
||||
diff --git a/src/bind_config.c b/src/bind_config.c
|
||||
index 9e4a7fd4..c12f1003 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 94b70d58..bf349812 100644
|
||||
--- a/src/config.c
|
||||
+++ b/src/config.c
|
||||
@@ -215,7 +215,7 @@ 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);
|
||||
@@ -1205,8 +1205,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 b022f09e..81219586 100644
|
||||
--- a/src/dh-gex.c
|
||||
+++ b/src/dh-gex.c
|
||||
@@ -520,9 +520,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 f660a6f3..ba2ae4d5 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 62777ad2..e4865ac2 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 7bb431b6..6607775e 100644
|
||||
--- a/src/misc.c
|
||||
+++ b/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 *hostname)
|
||||
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_TRACE,
|
||||
+ "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_TRACE,
|
||||
+ "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_TRACE,
|
||||
+ "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_TRACE,
|
||||
+ "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_TRACE,
|
||||
+ "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 995a5213..70ecfb95 100644
|
||||
--- a/tests/unittests/torture_config.c
|
||||
+++ b/tests/unittests/torture_config.c
|
||||
@@ -2237,6 +2237,23 @@ static void torture_config_parse_uri(void **state)
|
||||
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;
|
||||
@@ -2323,6 +2340,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),
|
||||
};
|
||||
|
||||
|
||||
--
|
||||
2.53.0
|
||||
|
||||
100
CVE-2026-0966.patch
Normal file
100
CVE-2026-0966.patch
Normal file
@ -0,0 +1,100 @@
|
||||
From e868036a8e496e36cf986e000e050974cc30a0ae Mon Sep 17 00:00:00 2001
|
||||
From: Jakub Jelen <jjelen@redhat.com>
|
||||
Date: Thu, 8 Jan 2026 12:09:50 +0100
|
||||
Subject: [PATCH 1/2] 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)
|
||||
---
|
||||
src/misc.c | 2 +-
|
||||
1 file changed, 1 insertion(+), 1 deletion(-)
|
||||
|
||||
diff --git a/src/misc.c b/src/misc.c
|
||||
index 6607775e..0cca373a 100644
|
||||
--- a/src/misc.c
|
||||
+++ b/src/misc.c
|
||||
@@ -452,7 +452,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.53.0
|
||||
|
||||
|
||||
From c112289ce14ef29f173d87b6cc507f066d6ca751 Mon Sep 17 00:00:00 2001
|
||||
From: Jakub Jelen <jjelen@redhat.com>
|
||||
Date: Thu, 8 Jan 2026 12:10:16 +0100
|
||||
Subject: [PATCH 2/2] 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)
|
||||
---
|
||||
tests/unittests/torture_misc.c | 31 +++++++++++++++++++++++++++++++
|
||||
1 file changed, 31 insertions(+)
|
||||
|
||||
diff --git a/tests/unittests/torture_misc.c b/tests/unittests/torture_misc.c
|
||||
index 77166759..4470c358 100644
|
||||
--- a/tests/unittests/torture_misc.c
|
||||
+++ b/tests/unittests/torture_misc.c
|
||||
@@ -877,6 +877,36 @@ static void torture_ssh_is_ipaddr(void **state) {
|
||||
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[] = {
|
||||
@@ -903,6 +933,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();
|
||||
--
|
||||
2.53.0
|
||||
|
||||
357
CVE-2026-0967.patch
Normal file
357
CVE-2026-0967.patch
Normal file
@ -0,0 +1,357 @@
|
||||
From a56c4837c9e6db6cfb7e136b0e8b8d90dbc329ea Mon Sep 17 00:00:00 2001
|
||||
From: Jakub Jelen <jjelen@redhat.com>
|
||||
Date: Wed, 17 Dec 2025 18:48:34 +0100
|
||||
Subject: [PATCH] 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)
|
||||
---
|
||||
src/match.c | 111 +++++++++++++----------------
|
||||
tests/unittests/torture_config.c | 116 +++++++++++++++++++++++--------
|
||||
2 files changed, 135 insertions(+), 92 deletions(-)
|
||||
|
||||
diff --git a/src/match.c b/src/match.c
|
||||
index 3e58f733..896d87cb 100644
|
||||
--- a/src/match.c
|
||||
+++ b/src/match.c
|
||||
@@ -43,85 +43,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');
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -172,7 +157,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 70ecfb95..6dc59e48 100644
|
||||
--- a/tests/unittests/torture_config.c
|
||||
+++ b/tests/unittests/torture_config.c
|
||||
@@ -1996,80 +1996,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.53.0
|
||||
|
||||
184
CVE-2026-0968.patch
Normal file
184
CVE-2026-0968.patch
Normal file
@ -0,0 +1,184 @@
|
||||
From 7d08a51ec529edc5ccf7978d403ec4ef151e82a7 Mon Sep 17 00:00:00 2001
|
||||
From: Jakub Jelen <jjelen@redhat.com>
|
||||
Date: Mon, 22 Dec 2025 20:59:11 +0100
|
||||
Subject: [PATCH 1/2] 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)
|
||||
---
|
||||
src/sftp.c | 16 +++++++++++++---
|
||||
1 file changed, 13 insertions(+), 3 deletions(-)
|
||||
|
||||
diff --git a/src/sftp.c b/src/sftp.c
|
||||
index 60e591f0..70f9ed15 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.53.0
|
||||
|
||||
|
||||
From ccedc7907ebe2c1a977b739b85034953de582fb3 Mon Sep 17 00:00:00 2001
|
||||
From: Jakub Jelen <jjelen@redhat.com>
|
||||
Date: Mon, 22 Dec 2025 21:00:03 +0100
|
||||
Subject: [PATCH 2/2] CVE-2026-0968 tests: Reproducer for invalid longname data
|
||||
|
||||
Signed-off-by: Jakub Jelen <jjelen@redhat.com>
|
||||
Reviewed-by: Andreas Schneider <asn@cryptomilk.org>
|
||||
(cherry picked from commit 90a5d8f47399e8db61b56793cd21476ff6a528e0)
|
||||
---
|
||||
tests/unittests/CMakeLists.txt | 7 +++
|
||||
tests/unittests/torture_unit_sftp.c | 86 +++++++++++++++++++++++++++++
|
||||
2 files changed, 93 insertions(+)
|
||||
create mode 100644 tests/unittests/torture_unit_sftp.c
|
||||
|
||||
diff --git a/tests/unittests/CMakeLists.txt b/tests/unittests/CMakeLists.txt
|
||||
index 28458d49..658f14bc 100644
|
||||
--- a/tests/unittests/CMakeLists.txt
|
||||
+++ b/tests/unittests/CMakeLists.txt
|
||||
@@ -95,6 +95,13 @@ if (UNIX AND NOT WIN32)
|
||||
endif (WITH_SERVER)
|
||||
endif (UNIX AND NOT WIN32)
|
||||
|
||||
+if (WITH_SFTP)
|
||||
+ set(LIBSSH_UNIT_TESTS
|
||||
+ ${LIBSSH_UNIT_TESTS}
|
||||
+ torture_unit_sftp
|
||||
+ )
|
||||
+endif (WITH_SFTP)
|
||||
+
|
||||
foreach(_UNIT_TEST ${LIBSSH_UNIT_TESTS})
|
||||
add_cmocka_test(${_UNIT_TEST}
|
||||
SOURCES ${_UNIT_TEST}.c
|
||||
diff --git a/tests/unittests/torture_unit_sftp.c b/tests/unittests/torture_unit_sftp.c
|
||||
new file mode 100644
|
||||
index 00000000..8cdaba8e
|
||||
--- /dev/null
|
||||
+++ b/tests/unittests/torture_unit_sftp.c
|
||||
@@ -0,0 +1,86 @@
|
||||
+#include "config.h"
|
||||
+
|
||||
+#include "sftp.c"
|
||||
+#include "torture.h"
|
||||
+
|
||||
+#define LIBSSH_STATIC
|
||||
+
|
||||
+static void test_sftp_parse_longname(void **state)
|
||||
+{
|
||||
+ const char *lname = NULL;
|
||||
+ char *value = NULL;
|
||||
+
|
||||
+ /* state not used */
|
||||
+ (void)state;
|
||||
+
|
||||
+ /* Valid example from SFTP draft, page 18:
|
||||
+ * https://datatracker.ietf.org/doc/draft-spaghetti-sshm-filexfer/
|
||||
+ */
|
||||
+ lname = "-rwxr-xr-x 1 mjos staff 348911 Mar 25 14:29 t-filexfer";
|
||||
+ value = sftp_parse_longname(lname, SFTP_LONGNAME_PERM);
|
||||
+ assert_string_equal(value, "-rwxr-xr-x");
|
||||
+ free(value);
|
||||
+ value = sftp_parse_longname(lname, SFTP_LONGNAME_OWNER);
|
||||
+ assert_string_equal(value, "mjos");
|
||||
+ free(value);
|
||||
+ value = sftp_parse_longname(lname, SFTP_LONGNAME_GROUP);
|
||||
+ assert_string_equal(value, "staff");
|
||||
+ free(value);
|
||||
+ value = sftp_parse_longname(lname, SFTP_LONGNAME_SIZE);
|
||||
+ assert_string_equal(value, "348911");
|
||||
+ free(value);
|
||||
+ /* This function is broken further as the date contains space which breaks
|
||||
+ * the parsing altogether */
|
||||
+ value = sftp_parse_longname(lname, SFTP_LONGNAME_DATE);
|
||||
+ assert_string_equal(value, "Mar");
|
||||
+ free(value);
|
||||
+ value = sftp_parse_longname(lname, SFTP_LONGNAME_TIME);
|
||||
+ assert_string_equal(value, "25");
|
||||
+ free(value);
|
||||
+ value = sftp_parse_longname(lname, SFTP_LONGNAME_NAME);
|
||||
+ assert_string_equal(value, "14:29");
|
||||
+ free(value);
|
||||
+}
|
||||
+
|
||||
+static void test_sftp_parse_longname_invalid(void **state)
|
||||
+{
|
||||
+ const char *lname = NULL;
|
||||
+ char *value = NULL;
|
||||
+
|
||||
+ /* state not used */
|
||||
+ (void)state;
|
||||
+
|
||||
+ /* Invalid inputs should not crash
|
||||
+ */
|
||||
+ lname = NULL;
|
||||
+ value = sftp_parse_longname(lname, SFTP_LONGNAME_PERM);
|
||||
+ assert_null(value);
|
||||
+ value = sftp_parse_longname(lname, SFTP_LONGNAME_NAME);
|
||||
+ assert_null(value);
|
||||
+
|
||||
+ lname = "";
|
||||
+ value = sftp_parse_longname(lname, SFTP_LONGNAME_PERM);
|
||||
+ assert_string_equal(value, "");
|
||||
+ free(value);
|
||||
+ value = sftp_parse_longname(lname, SFTP_LONGNAME_NAME);
|
||||
+ assert_null(value);
|
||||
+
|
||||
+ lname = "-rwxr-xr-x 1";
|
||||
+ value = sftp_parse_longname(lname, SFTP_LONGNAME_PERM);
|
||||
+ assert_string_equal(value, "-rwxr-xr-x");
|
||||
+ free(value);
|
||||
+ value = sftp_parse_longname(lname, SFTP_LONGNAME_NAME);
|
||||
+ assert_null(value);
|
||||
+}
|
||||
+
|
||||
+int torture_run_tests(void)
|
||||
+{
|
||||
+ int rc;
|
||||
+ const struct CMUnitTest tests[] = {
|
||||
+ cmocka_unit_test(test_sftp_parse_longname),
|
||||
+ cmocka_unit_test(test_sftp_parse_longname_invalid),
|
||||
+ };
|
||||
+
|
||||
+ rc = cmocka_run_group_tests(tests, NULL, NULL);
|
||||
+ return rc;
|
||||
+}
|
||||
--
|
||||
2.53.0
|
||||
|
||||
25
libssh.spec
25
libssh.spec
@ -1,6 +1,6 @@
|
||||
Name: libssh
|
||||
Version: 0.10.4
|
||||
Release: 17%{?dist}
|
||||
Release: 18%{?dist}
|
||||
Summary: A library implementing the SSH protocol
|
||||
License: LGPLv2+
|
||||
URL: http://www.libssh.org
|
||||
@ -58,6 +58,16 @@ Patch16: escape-brackets-in-proxycommand.patch
|
||||
Patch17: CVE-2025-5318.patch
|
||||
Patch18: CVE-2025-5987.patch
|
||||
Patch19: workaround-sshd-failure-rate-limiting.patch
|
||||
Patch20: CVE-2025-4877.patch
|
||||
Patch21: CVE-2025-4878.patch
|
||||
Patch22: CVE-2025-5351.patch
|
||||
Patch23: CVE-2025-8114.patch
|
||||
Patch24: CVE-2025-8277.patch
|
||||
Patch25: CVE-2026-0964.patch
|
||||
Patch26: CVE-2026-0965.patch
|
||||
Patch27: CVE-2026-0966.patch
|
||||
Patch28: CVE-2026-0967.patch
|
||||
Patch29: CVE-2026-0968.patch
|
||||
|
||||
%description
|
||||
The ssh library was designed to be used by programmers needing a working SSH
|
||||
@ -150,6 +160,19 @@ popd
|
||||
%attr(0644,root,root) %config(noreplace) %{_sysconfdir}/libssh/libssh_server.config
|
||||
|
||||
%changelog
|
||||
* Fri Feb 13 2026 Pavol Žáčik <pzacik@redhat.com> - 0.10.4-18
|
||||
- Resolves: RHEL-150661
|
||||
- Resolves: CVE-2025-4877
|
||||
- Resolves: CVE-2025-4878
|
||||
- Resolves: CVE-2025-5351
|
||||
- Resolves: CVE-2025-8114
|
||||
- Resolves: CVE-2025-8277
|
||||
- Resolves: CVE-2026-0964
|
||||
- Resolves: CVE-2026-0965
|
||||
- Resolves: CVE-2026-0966
|
||||
- Resolves: CVE-2026-0967
|
||||
- Resolves: CVE-2026-0968
|
||||
|
||||
* Fri Dec 12 2025 Pavol Žáčik <pzacik@redhat.com> - 0.10.4-17
|
||||
- Bump spec to resolve build tagging issues
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user