Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions SPECS/libssh/CVE-2026-0964.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
From 89fc59935d26f32bbd6d745c2042eb4e888e1b67 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)
Signed-off-by: Azure Linux Security Servicing Account <azurelinux-security@microsoft.com>
Upstream-reference: https://git.libssh.org/projects/libssh.git/patch/?id=a5e4b12090b0c939d85af4f29280e40c5b6600aa
---
src/scp.c | 16 ++++++++++++++++
1 file changed, 16 insertions(+)

diff --git a/src/scp.c b/src/scp.c
index 103822c..c23b7b1 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.45.4

278 changes: 278 additions & 0 deletions SPECS/libssh/CVE-2026-0965.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
From 268894dd119cab18560a88dc774baa0129650be9 Mon Sep 17 00:00:00 2001
From: Azure Linux Security Servicing Account
<azurelinux-security@microsoft.com>
Date: Wed, 1 Apr 2026 19:50:22 +0000
Subject: [PATCH] Patch CVE-2026-0965 for libssh (applied via patch -p1)

Signed-off-by: Azure Linux Security Servicing Account <azurelinux-security@microsoft.com>
Upstream-reference: https://git.libssh.org/projects/libssh.git/patch/?id=bf390a042623e02abc8f421c4c5fadc0429a8a76
---
include/libssh/misc.h | 4 ++
include/libssh/priv.h | 3 ++
src/bind_config.c | 4 +-
src/config.c | 8 ++--
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, 110 insertions(+), 10 deletions(-)

diff --git a/include/libssh/misc.h b/include/libssh/misc.h
index 0924ba7..27bdc2d 100644
--- a/include/libssh/misc.h
+++ b/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, 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);
+
#ifdef __cplusplus
}
#endif
diff --git a/include/libssh/priv.h b/include/libssh/priv.h
index 47af57f..b55df50 100644
--- a/include/libssh/priv.h
+++ b/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
diff --git a/src/bind_config.c b/src/bind_config.c
index ed42cbe..c429bce 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 d4d8d41..87cdaaa 100644
--- a/src/config.c
+++ b/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 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 642a88a..aadc7c0 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 f660a6f..ba2ae4d 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 109b4f0..f0fde69 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_WARN, "Failed to open the known_hosts file '%s': %s",
diff --git a/src/misc.c b/src/misc.c
index f371f33..d936385 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_RARE,
+ "Failed to open a file %s for reading: %s",
+ filename,
+ ssh_strerror(errno, err_msg, SSH_ERRNO_MSG_MAX));
+ return NULL;
+ }
+
+ /* Check the file is sensible for a configuration file */
+ r = fstat(fd, &sb);
+ if (r != 0) {
+ SSH_LOG(SSH_LOG_RARE,
+ "Failed to stat %s: %s",
+ filename,
+ ssh_strerror(errno, err_msg, SSH_ERRNO_MSG_MAX));
+ close(fd);
+ return NULL;
+ }
+ if ((sb.st_mode & S_IFMT) != S_IFREG) {
+ SSH_LOG(SSH_LOG_RARE,
+ "The file %s is not a regular file: skipping",
+ filename);
+ close(fd);
+ return NULL;
+ }
+
+ if ((size_t)sb.st_size > max_file_size) {
+ SSH_LOG(SSH_LOG_RARE,
+ "The file %s is too large (%jd MB > %zu MB): skipping",
+ filename,
+ (intmax_t)sb.st_size / 1024 / 1024,
+ max_file_size / 1024 / 1024);
+ close(fd);
+ return NULL;
+ }
+
+ f = fdopen(fd, "r");
+ if (f == NULL) {
+ SSH_LOG(SSH_LOG_RARE,
+ "Failed to open a file %s for reading: %s",
+ filename,
+ ssh_strerror(r, err_msg, SSH_ERRNO_MSG_MAX));
+ close(fd);
+ return NULL;
+ }
+
+ /* the flcose() will close also the underlying fd */
+ return f;
+}
+
/** @} */
diff --git a/tests/unittests/torture_config.c b/tests/unittests/torture_config.c
index b7c763a..b218f9e 100644
--- a/tests/unittests/torture_config.c
+++ b/tests/unittests/torture_config.c
@@ -1850,6 +1850,22 @@ static void torture_config_make_absolute_no_sshdir(void **state)
torture_config_make_absolute_int(state, 1);
}

+/* 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;
@@ -1922,6 +1938,9 @@ int torture_run_tests(void)
setup, teardown),
cmocka_unit_test_setup_teardown(torture_config_make_absolute_no_sshdir,
setup_no_sshdir, teardown),
+ cmocka_unit_test_setup_teardown(torture_config_invalid,
+ setup,
+ teardown),
};


--
2.45.4

34 changes: 34 additions & 0 deletions SPECS/libssh/CVE-2026-0966.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
From 1fd7f07af7dc69d4b86dabde62dc5ecbf449cfd7 Mon Sep 17 00:00:00 2001
From: Jakub Jelen <jjelen@redhat.com>
Date: Thu, 8 Jan 2026 12:09:50 +0100
Subject: [PATCH] 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)
Signed-off-by: Azure Linux Security Servicing Account <azurelinux-security@microsoft.com>
Upstream-reference: https://git.libssh.org/projects/libssh.git/patch/?id=6ba5ff1b7b1547a59f750fbc06b89737b7456117
---
src/misc.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/misc.c b/src/misc.c
index d936385..e78c92b 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.45.4

Loading
Loading