From e1cb1edddf2306b889515368668d2faa004fe921 Mon Sep 17 00:00:00 2001 From: Nuhiat-Arefin Date: Wed, 22 Apr 2026 10:47:32 +0600 Subject: [PATCH] knownhosts: restrict StrictHostKeyChecking off on key mismatches Keep the unknown host handling under StrictHostKeyChecking off, including writing accepted keys through to known_hosts. For changed host keys and different stored key types, do not treat the host as fully trusted. In that path, disable password and keyboard interactive authentication before returning OK. Apply the same handling in both ssh_is_server_known() and ssh_session_get_known_hosts_entry(). Signed-off-by: Nuhiat-Arefin Reviewed-by: Jakub Jelen Merge-Request: --- include/libssh/session.h | 24 +++ src/known_hosts.c | 19 +- src/knownhosts.c | 32 +++- tests/client/torture_knownhosts.c | 142 ++++++++++++++ tests/client/torture_knownhosts_verify.c | 226 +++++++++++++++++++++-- 5 files changed, 422 insertions(+), 21 deletions(-) diff --git a/include/libssh/session.h b/include/libssh/session.h index f926b223..b604b008 100644 --- a/include/libssh/session.h +++ b/include/libssh/session.h @@ -311,6 +311,30 @@ struct ssh_session_struct { struct ssh_pki_ctx_struct *pki_context; }; +/** + * @internal + * + * @brief Apply the reduced-authentication behavior for an unsafe host key + * continuation. + * + * When `StrictHostKeyChecking` is `off` and host key verification reports a + * changed or other key, OpenSSH allows the connection to continue only + * "subject to some restrictions"; see `ssh_config(5)` for + * `StrictHostKeyChecking` and `check_host_key()` in OpenSSH's `sshconnect.c` + * under the `continue_unsafe` label. For libssh's supported client-side auth + * methods, this means disabling password and keyboard-interactive + * authentication for the rest of the session. + */ +static inline void ssh_known_hosts_continue_unsafe(ssh_session session) +{ + SSH_LOG(SSH_LOG_WARN, + "Continuing despite an unsafe host key with " + "StrictHostKeyChecking=off; password and keyboard-interactive " + "authentication have been disabled for this session"); + session->opts.flags &= + ~(SSH_OPT_FLAG_PASSWORD_AUTH | SSH_OPT_FLAG_KBDINT_AUTH); +} + /** @internal * @brief a termination function evaluates the status of an object * @param user[in] object to evaluate diff --git a/src/known_hosts.c b/src/known_hosts.c index 4375a5dd..82578199 100644 --- a/src/known_hosts.c +++ b/src/known_hosts.c @@ -419,10 +419,21 @@ int ssh_is_server_known(ssh_session session) } } while (1); - if ((ret == SSH_SERVER_NOT_KNOWN) && - (session->opts.StrictHostKeyChecking == SSH_STRICT_HOSTKEY_OFF || - session->opts.StrictHostKeyChecking == - SSH_STRICT_HOSTKEY_ACCEPT_NEW)) { + if (session->opts.StrictHostKeyChecking == SSH_STRICT_HOSTKEY_OFF) { + if (ret == SSH_SERVER_KNOWN_CHANGED || ret == SSH_SERVER_FOUND_OTHER) { + ssh_known_hosts_continue_unsafe(session); + ret = SSH_SERVER_KNOWN_OK; + } else if (ret == SSH_SERVER_NOT_KNOWN) { + int rv = ssh_session_update_known_hosts(session); + if (rv != SSH_OK) { + ret = SSH_SERVER_ERROR; + } else { + ret = SSH_SERVER_KNOWN_OK; + } + } + } else if ((ret == SSH_SERVER_NOT_KNOWN) && + session->opts.StrictHostKeyChecking == + SSH_STRICT_HOSTKEY_ACCEPT_NEW) { int rv = ssh_session_update_known_hosts(session); if (rv != SSH_OK) { ret = SSH_SERVER_ERROR; diff --git a/src/knownhosts.c b/src/knownhosts.c index df057f1b..63345103 100644 --- a/src/knownhosts.c +++ b/src/knownhosts.c @@ -386,6 +386,7 @@ static void ssh_knownhosts_entries_free(struct ssh_list *entry_list) } ssh_list_free(entry_list); } + /** * @internal * @@ -1152,6 +1153,13 @@ ssh_known_hosts_check_server_key(const char *hosts_entry, return found; } +static bool ssh_known_hosts_should_continue_unsafe(ssh_session session, + enum ssh_known_hosts_e rv) +{ + return session->opts.StrictHostKeyChecking == SSH_STRICT_HOSTKEY_OFF && + (rv == SSH_KNOWN_HOSTS_CHANGED || rv == SSH_KNOWN_HOSTS_OTHER); +} + /** * @brief Get the known_hosts entry for the currently connected session. * @@ -1210,8 +1218,18 @@ ssh_session_get_known_hosts_entry(ssh_session session, session->opts.global_knownhosts, pentry); + if (ssh_known_hosts_should_continue_unsafe(session, rv)) { + ssh_known_hosts_continue_unsafe(session); + return SSH_KNOWN_HOSTS_OK; + } + /* If the global file did not help, report the result from the user file. */ if (rv == SSH_KNOWN_HOSTS_UNKNOWN || rv == SSH_KNOWN_HOSTS_NOT_FOUND) { + if (ssh_known_hosts_should_continue_unsafe(session, old_rv)) { + ssh_known_hosts_continue_unsafe(session); + return SSH_KNOWN_HOSTS_OK; + } + if ((old_rv == SSH_KNOWN_HOSTS_UNKNOWN || old_rv == SSH_KNOWN_HOSTS_NOT_FOUND) && (session->opts.StrictHostKeyChecking == SSH_STRICT_HOSTKEY_OFF || @@ -1222,10 +1240,16 @@ ssh_session_get_known_hosts_entry(ssh_session session, return SSH_KNOWN_HOSTS_ERROR; } - return ssh_session_get_known_hosts_entry_file( - session, - session->opts.knownhosts, - pentry); + rv = + ssh_session_get_known_hosts_entry_file(session, + session->opts.knownhosts, + pentry); + if (rv == SSH_KNOWN_HOSTS_UNKNOWN || + rv == SSH_KNOWN_HOSTS_NOT_FOUND) { + return SSH_KNOWN_HOSTS_OK; + } + + return rv; } return old_rv; diff --git a/tests/client/torture_knownhosts.c b/tests/client/torture_knownhosts.c index 55aee217..a0e82384 100644 --- a/tests/client/torture_knownhosts.c +++ b/tests/client/torture_knownhosts.c @@ -29,6 +29,7 @@ #include #include #include +#include #include "session.c" #include "known_hosts.c" @@ -474,6 +475,135 @@ static void torture_knownhosts_no_hostkeychecking(void **state) assert_int_equal(found, SSH_KNOWN_HOSTS_OK); } +static void torture_knownhosts_no_hostkeychecking_legacy(void **state) +{ + struct torture_state *s = *state; + ssh_session session = s->ssh.session; + struct stat sb; + char tmp_file[1024] = {0}; + char *known_hosts_file = NULL; + int strict_host_key_checking = SSH_STRICT_HOSTKEY_OFF; + int rc; + + snprintf(tmp_file, + sizeof(tmp_file), + "%s/%s", + s->socket_dir, + TMP_FILE_TEMPLATE); + + known_hosts_file = torture_create_temp_file(tmp_file); + assert_non_null(known_hosts_file); + + rc = ssh_options_set(session, SSH_OPTIONS_KNOWNHOSTS, known_hosts_file); + assert_ssh_return_code(session, rc); + + rc = ssh_options_set(session, SSH_OPTIONS_HOSTKEYS, "ecdsa-sha2-nistp521"); + assert_ssh_return_code(session, rc); + + rc = ssh_options_set(session, + SSH_OPTIONS_STRICTHOSTKEYCHECK, + &strict_host_key_checking); + assert_ssh_return_code(session, rc); + + rc = ssh_connect(session); + assert_ssh_return_code(session, rc); + + rc = ssh_is_server_known(session); + assert_int_equal(rc, SSH_SERVER_KNOWN_OK); + + rc = stat(known_hosts_file, &sb); + assert_return_code(rc, errno); + assert_true(sb.st_size > 0); + + free(known_hosts_file); +} + +static void torture_knownhosts_no_hostkeychecking_changed(void **state) +{ + struct torture_state *s = *state; + ssh_session session = s->ssh.session; + char tmp_file[1024] = {0}; + char *known_hosts_file = NULL; + int strict_host_key_checking = SSH_STRICT_HOSTKEY_OFF; + int rc; + + snprintf(tmp_file, + sizeof(tmp_file), + "%s/%s", + s->socket_dir, + TMP_FILE_TEMPLATE); + + known_hosts_file = torture_create_temp_file(tmp_file); + assert_non_null(known_hosts_file); + + rc = ssh_options_set(session, SSH_OPTIONS_KNOWNHOSTS, known_hosts_file); + assert_ssh_return_code(session, rc); + + rc = ssh_options_set(session, SSH_OPTIONS_HOSTKEYS, "rsa-sha2-256"); + assert_ssh_return_code(session, rc); + + rc = ssh_options_set(session, + SSH_OPTIONS_STRICTHOSTKEYCHECK, + &strict_host_key_checking); + assert_ssh_return_code(session, rc); + + torture_write_file(known_hosts_file, + TORTURE_SSH_SERVER " ssh-rsa " BADRSA "\n"); + + rc = ssh_connect(session); + assert_ssh_return_code(session, rc); + + rc = ssh_is_server_known(session); + assert_int_equal(rc, SSH_SERVER_KNOWN_OK); + assert_false(session->opts.flags & SSH_OPT_FLAG_PASSWORD_AUTH); + assert_false(session->opts.flags & SSH_OPT_FLAG_KBDINT_AUTH); + + free(known_hosts_file); +} + +static void torture_knownhosts_no_hostkeychecking_other(void **state) +{ + struct torture_state *s = *state; + ssh_session session = s->ssh.session; + char tmp_file[1024] = {0}; + char *known_hosts_file = NULL; + int strict_host_key_checking = SSH_STRICT_HOSTKEY_OFF; + int rc; + + snprintf(tmp_file, + sizeof(tmp_file), + "%s/%s", + s->socket_dir, + TMP_FILE_TEMPLATE); + + known_hosts_file = torture_create_temp_file(tmp_file); + assert_non_null(known_hosts_file); + + rc = ssh_options_set(session, SSH_OPTIONS_KNOWNHOSTS, known_hosts_file); + assert_ssh_return_code(session, rc); + + rc = ssh_options_set(session, SSH_OPTIONS_HOSTKEYS, "ecdsa-sha2-nistp521"); + assert_ssh_return_code(session, rc); + + rc = ssh_options_set(session, + SSH_OPTIONS_STRICTHOSTKEYCHECK, + &strict_host_key_checking); + assert_ssh_return_code(session, rc); + + torture_write_file(known_hosts_file, + TORTURE_SSH_SERVER " ssh-rsa " BADRSA "\n"); + + rc = ssh_connect(session); + assert_ssh_return_code(session, rc); + + rc = ssh_is_server_known(session); + assert_int_equal(rc, SSH_SERVER_KNOWN_OK); + assert_false(session->opts.flags & SSH_OPT_FLAG_PASSWORD_AUTH); + assert_false(session->opts.flags & SSH_OPT_FLAG_KBDINT_AUTH); + + free(known_hosts_file); +} + int torture_run_tests(void) { int rc; struct CMUnitTest tests[] = { @@ -501,6 +631,18 @@ int torture_run_tests(void) { cmocka_unit_test_setup_teardown(torture_knownhosts_no_hostkeychecking, session_setup, session_teardown), + cmocka_unit_test_setup_teardown( + torture_knownhosts_no_hostkeychecking_legacy, + session_setup, + session_teardown), + cmocka_unit_test_setup_teardown( + torture_knownhosts_no_hostkeychecking_changed, + session_setup, + session_teardown), + cmocka_unit_test_setup_teardown( + torture_knownhosts_no_hostkeychecking_other, + session_setup, + session_teardown), }; ssh_init(); diff --git a/tests/client/torture_knownhosts_verify.c b/tests/client/torture_knownhosts_verify.c index 25ed9920..98c58b5b 100644 --- a/tests/client/torture_knownhosts_verify.c +++ b/tests/client/torture_knownhosts_verify.c @@ -424,18 +424,207 @@ static void torture_knownhosts_accept_new_persists(void **state) free(known_hosts_file); } -static void torture_knownhosts_accept_new_rejects_changed(void **state) +static void torture_knownhosts_no_hostkeychecking_dev_null(void **state) +{ + struct torture_state *s = *state; + ssh_session session = s->ssh.session; + enum ssh_known_hosts_e found; + int strict_host_key_checking = SSH_STRICT_HOSTKEY_OFF; + int rc; + + rc = ssh_options_set(session, SSH_OPTIONS_KNOWNHOSTS, "/dev/null"); + assert_ssh_return_code(session, rc); + + rc = ssh_options_set(session, SSH_OPTIONS_GLOBAL_KNOWNHOSTS, "/dev/null"); + assert_ssh_return_code(session, rc); + + rc = ssh_options_set(session, SSH_OPTIONS_HOSTKEYS, "ecdsa-sha2-nistp521"); + assert_ssh_return_code(session, rc); + + rc = ssh_options_set(session, + SSH_OPTIONS_STRICTHOSTKEYCHECK, + &strict_host_key_checking); + assert_ssh_return_code(session, rc); + + rc = ssh_connect(session); + assert_ssh_return_code(session, rc); + + found = ssh_session_get_known_hosts_entry(session, NULL); + assert_int_equal(found, SSH_KNOWN_HOSTS_OK); +} + +static void torture_knownhosts_no_hostkeychecking_changed(void **state) { struct torture_state *s = *state; ssh_session session = s->ssh.session; - struct ssh_knownhosts_entry *entry = NULL; - struct stat sb_before; - struct stat sb_after; char tmp_file[1024] = {0}; char *known_hosts_file = NULL; enum ssh_known_hosts_e found; - FILE *file = NULL; - int strict_host_key_checking = SSH_STRICT_HOSTKEY_NEW; + int strict_host_key_checking = SSH_STRICT_HOSTKEY_OFF; + int rc; + + snprintf(tmp_file, + sizeof(tmp_file), + "%s/%s", + s->socket_dir, + TMP_FILE_TEMPLATE); + + known_hosts_file = torture_create_temp_file(tmp_file); + assert_non_null(known_hosts_file); + + torture_write_file(known_hosts_file, + TORTURE_SSH_SERVER " ssh-rsa " BAD_RSA "\n"); + + rc = ssh_options_set(session, SSH_OPTIONS_KNOWNHOSTS, known_hosts_file); + assert_ssh_return_code(session, rc); + + rc = ssh_options_set(session, SSH_OPTIONS_HOSTKEYS, "rsa-sha2-256"); + assert_ssh_return_code(session, rc); + + rc = ssh_options_set(session, + SSH_OPTIONS_STRICTHOSTKEYCHECK, + &strict_host_key_checking); + assert_ssh_return_code(session, rc); + + rc = ssh_connect(session); + assert_ssh_return_code(session, rc); + + found = ssh_session_get_known_hosts_entry(session, NULL); + assert_int_equal(found, SSH_KNOWN_HOSTS_OK); + assert_false(session->opts.flags & SSH_OPT_FLAG_PASSWORD_AUTH); + assert_false(session->opts.flags & SSH_OPT_FLAG_KBDINT_AUTH); + + free(known_hosts_file); +} + +static void torture_knownhosts_no_hostkeychecking_other(void **state) +{ + struct torture_state *s = *state; + ssh_session session = s->ssh.session; + char tmp_file[1024] = {0}; + char *known_hosts_file = NULL; + enum ssh_known_hosts_e found; + int strict_host_key_checking = SSH_STRICT_HOSTKEY_OFF; + int rc; + + snprintf(tmp_file, + sizeof(tmp_file), + "%s/%s", + s->socket_dir, + TMP_FILE_TEMPLATE); + + known_hosts_file = torture_create_temp_file(tmp_file); + assert_non_null(known_hosts_file); + + torture_write_file(known_hosts_file, + TORTURE_SSH_SERVER " ssh-rsa " BAD_RSA "\n"); + + rc = ssh_options_set(session, SSH_OPTIONS_KNOWNHOSTS, known_hosts_file); + assert_ssh_return_code(session, rc); + + rc = ssh_options_set(session, SSH_OPTIONS_HOSTKEYS, "ecdsa-sha2-nistp521"); + assert_ssh_return_code(session, rc); + + rc = ssh_options_set(session, + SSH_OPTIONS_STRICTHOSTKEYCHECK, + &strict_host_key_checking); + assert_ssh_return_code(session, rc); + + rc = ssh_connect(session); + assert_ssh_return_code(session, rc); + + found = ssh_session_get_known_hosts_entry(session, NULL); + assert_int_equal(found, SSH_KNOWN_HOSTS_OK); + assert_false(session->opts.flags & SSH_OPT_FLAG_PASSWORD_AUTH); + assert_false(session->opts.flags & SSH_OPT_FLAG_KBDINT_AUTH); + + free(known_hosts_file); +} + +static void +torture_knownhosts_no_hostkeychecking_changed_global_ok(void **state) +{ + struct torture_state *s = *state; + ssh_session session = s->ssh.session; + struct ssh_knownhosts_entry *entry = NULL; + char global_tmp_file[1024] = {0}; + char user_tmp_file[1024] = {0}; + char *global_known_hosts_file = NULL; + char *known_hosts_entry = NULL; + char *user_known_hosts_file = NULL; + enum ssh_known_hosts_e found; + int strict_host_key_checking = SSH_STRICT_HOSTKEY_OFF; + int rc; + + snprintf(user_tmp_file, + sizeof(user_tmp_file), + "%s/%s", + s->socket_dir, + TMP_FILE_TEMPLATE); + user_known_hosts_file = torture_create_temp_file(user_tmp_file); + assert_non_null(user_known_hosts_file); + + snprintf(global_tmp_file, + sizeof(global_tmp_file), + "%s/%s", + s->socket_dir, + TMP_FILE_TEMPLATE); + global_known_hosts_file = torture_create_temp_file(global_tmp_file); + assert_non_null(global_known_hosts_file); + + torture_write_file(user_known_hosts_file, + TORTURE_SSH_SERVER " ssh-rsa " BAD_RSA "\n"); + + rc = + ssh_options_set(session, SSH_OPTIONS_KNOWNHOSTS, user_known_hosts_file); + assert_ssh_return_code(session, rc); + + rc = ssh_options_set(session, + SSH_OPTIONS_GLOBAL_KNOWNHOSTS, + global_known_hosts_file); + assert_ssh_return_code(session, rc); + + rc = ssh_options_set(session, SSH_OPTIONS_HOSTKEYS, "rsa-sha2-256"); + assert_ssh_return_code(session, rc); + + rc = ssh_options_set(session, + SSH_OPTIONS_STRICTHOSTKEYCHECK, + &strict_host_key_checking); + assert_ssh_return_code(session, rc); + + rc = ssh_connect(session); + assert_ssh_return_code(session, rc); + + rc = ssh_session_export_known_hosts_entry(session, &known_hosts_entry); + assert_ssh_return_code(session, rc); + torture_write_file(global_known_hosts_file, known_hosts_entry); + SAFE_FREE(known_hosts_entry); + + assert_true(session->opts.flags & SSH_OPT_FLAG_PASSWORD_AUTH); + assert_true(session->opts.flags & SSH_OPT_FLAG_KBDINT_AUTH); + + found = ssh_session_get_known_hosts_entry(session, &entry); + assert_int_equal(found, SSH_KNOWN_HOSTS_OK); + assert_non_null(entry); + assert_true(session->opts.flags & SSH_OPT_FLAG_PASSWORD_AUTH); + assert_true(session->opts.flags & SSH_OPT_FLAG_KBDINT_AUTH); + + ssh_knownhosts_entry_free(entry); + free(global_known_hosts_file); + free(user_known_hosts_file); +} + +static void torture_knownhosts_accept_new_rejects_changed(void **state) +{ + struct torture_state *s = *state; + ssh_session session = s->ssh.session; + struct ssh_knownhosts_entry *entry = NULL; + struct stat sb_before; + struct stat sb_after; + char tmp_file[1024] = {0}; + char *known_hosts_file = NULL; + enum ssh_known_hosts_e found; + int strict_host_key_checking = SSH_STRICT_HOSTKEY_ACCEPT_NEW; int rc; snprintf(tmp_file, @@ -447,14 +636,9 @@ static void torture_knownhosts_accept_new_rejects_changed(void **state) known_hosts_file = torture_create_temp_file(tmp_file); assert_non_null(known_hosts_file); - file = fopen(known_hosts_file, "w"); - if (file == NULL) { - free(known_hosts_file); - fail(); - } /* BAD_RSA is a fixed fixture key that must not match the test server. */ - fprintf(file, "127.0.0.10 %s %s\n", "ssh-rsa", BAD_RSA); - fclose(file); + torture_write_file(known_hosts_file, + TORTURE_SSH_SERVER " ssh-rsa " BAD_RSA "\n"); rc = stat(known_hosts_file, &sb_before); assert_return_code(rc, errno); @@ -615,6 +799,22 @@ int torture_run_tests(void) { cmocka_unit_test_setup_teardown(torture_knownhosts_accept_new_persists, session_setup, session_teardown), + cmocka_unit_test_setup_teardown( + torture_knownhosts_no_hostkeychecking_dev_null, + session_setup, + session_teardown), + cmocka_unit_test_setup_teardown( + torture_knownhosts_no_hostkeychecking_changed, + session_setup, + session_teardown), + cmocka_unit_test_setup_teardown( + torture_knownhosts_no_hostkeychecking_other, + session_setup, + session_teardown), + cmocka_unit_test_setup_teardown( + torture_knownhosts_no_hostkeychecking_changed_global_ok, + session_setup, + session_teardown), cmocka_unit_test_setup_teardown( torture_knownhosts_accept_new_rejects_changed, session_setup,