diff --git a/include/libssh/misc.h b/include/libssh/misc.h index b7716a5f..6e959cdd 100644 --- a/include/libssh/misc.h +++ b/include/libssh/misc.h @@ -57,6 +57,7 @@ char *ssh_path_expand_escape(ssh_session session, const char *s); int ssh_analyze_banner(ssh_session session, int server); int ssh_is_ipaddr_v4(const char *str); int ssh_is_ipaddr(const char *str); +int ssh_normalize_loose_ip(const char *host, char **result); /* list processing */ diff --git a/src/misc.c b/src/misc.c index b4d69493..4786ae90 100644 --- a/src/misc.c +++ b/src/misc.c @@ -2686,4 +2686,67 @@ size_t strlcat(char *dst, const char *src, size_t size) } #endif /* HAVE_STRLCAT */ +/** + * @brief Normalizes a loose IPv4 string (e.g. "0", "127.1") to dotted-quad + * format. + * + * @param[in] host The hostname string to normalize. + * @param[out] result On success (returns 0), set to a newly allocated + * dotted-quad string. The caller must free it. + * Untouched on other return values. + * + * @return 0 if the input was a loose IPv4 and was normalized successfully. + * 1 if the input is not a loose IPv4 address (not an error). + * -1 on error (NULL input or internal failure). + */ +int ssh_normalize_loose_ip(const char *host, char **result) +{ + struct in_addr addr; + char buf[INET_ADDRSTRLEN]; + const char *p = NULL; + int rc; + int is_ip; +#ifdef _WIN32 + unsigned long ip; + int is_broadcast; +#endif + + if (host == NULL || result == NULL) { + return -1; + } + + /* We don't want to normalize stricter IP checks already handled by valid + * IPv4/IPv6 */ + is_ip = ssh_is_ipaddr(host); + if (is_ip) { + return 1; /* not a loose IP — already a strict address */ + } + +#ifdef _WIN32 + ip = inet_addr(host); + is_broadcast = strcmp(host, "255.255.255.255"); + if (ip == INADDR_NONE && is_broadcast != 0) { + return 1; /* not a loose IP */ + } + addr.S_un.S_addr = ip; +#else + rc = inet_aton(host, &addr); + if (rc == 0) { + return 1; /* not a loose IP */ + } +#endif + + p = inet_ntop(AF_INET, &addr, buf, sizeof(buf)); + if (p == NULL) { + return -1; + } + + *result = strdup(p); + if (*result == NULL) { + return -1; + } + + return 0; +} + /** @} */ diff --git a/src/options.c b/src/options.c index a25c976d..b2dbc05f 100644 --- a/src/options.c +++ b/src/options.c @@ -735,7 +735,8 @@ int ssh_options_set_algo(ssh_session session, * to a pointer when it should have just been a pointer), then the * behaviour is undefined. */ -int ssh_options_set(ssh_session session, enum ssh_options_e type, +int ssh_options_set(ssh_session session, + enum ssh_options_e type, const void *value) { const char *v = NULL; @@ -759,6 +760,7 @@ int ssh_options_set(ssh_session session, enum ssh_options_e type, } else { char *username = NULL, *hostname = NULL; char *strict_hostname = NULL; + char *normalized = NULL; /* Non-strict parse: reject shell metacharacters */ rc = ssh_config_parse_uri(value, @@ -787,12 +789,22 @@ int ssh_options_set(ssh_session session, enum ssh_options_e type, } /* Strict parse: set host only if valid hostname or IP */ - rc = ssh_config_parse_uri(value, - NULL, - &strict_hostname, - NULL, - true, - true); + rc = ssh_normalize_loose_ip(value, &normalized); + if (rc == -1) { + /* Error */ + SAFE_FREE(username); + ssh_set_error_oom(session); + return -1; + } + rc = ssh_config_parse_uri( + (normalized != NULL) ? normalized : value, + NULL, + &strict_hostname, + NULL, + true, + true); + SAFE_FREE(normalized); + if (rc != SSH_OK || strict_hostname == NULL) { SAFE_FREE(session->opts.host); SAFE_FREE(strict_hostname); @@ -2222,6 +2234,23 @@ int ssh_options_apply(ssh_session session) char *tmp = NULL; int rc; + if (session->opts.host != NULL) { + char *normalized_host = ssh_normalize_loose_ip(session->opts.host); + if (normalized_host != NULL) { + SAFE_FREE(session->opts.host); + session->opts.host = normalized_host; + } else { + bool is_ip = ssh_is_ipaddr(session->opts.host); + if (!is_ip) { + char *lower = ssh_lowercase(session->opts.host); + if (lower != NULL) { + SAFE_FREE(session->opts.host); + session->opts.host = lower; + } + } + } + } + if (session->opts.sshdir == NULL) { rc = ssh_options_set(session, SSH_OPTIONS_SSH_DIR, NULL); if (rc < 0) { diff --git a/tests/unittests/torture_misc.c b/tests/unittests/torture_misc.c index 788efb62..901dbc53 100644 --- a/tests/unittests/torture_misc.c +++ b/tests/unittests/torture_misc.c @@ -1345,6 +1345,90 @@ static void torture_strlcat(void **state) assert_string_equal(dest, "test"); } +static void torture_ssh_normalize_loose_ip(UNUSED_PARAM(void **state)) +{ + char *result = NULL; + int rc; + + /* NULL args should return -1 (error) */ + rc = ssh_normalize_loose_ip(NULL, &result); + assert_int_equal(rc, -1); + assert_null(result); + + rc = ssh_normalize_loose_ip("127.0.0.1", NULL); + assert_int_equal(rc, -1); + + /* Loose IPv4 forms — should normalize to dotted-quad (rc=0) */ + result = NULL; + rc = ssh_normalize_loose_ip("0", &result); + assert_int_equal(rc, 0); + assert_non_null(result); + assert_string_equal(result, "0.0.0.0"); + SAFE_FREE(result); + + result = NULL; + rc = ssh_normalize_loose_ip("127.1", &result); + assert_int_equal(rc, 0); + assert_non_null(result); + assert_string_equal(result, "127.0.0.1"); + SAFE_FREE(result); + + result = NULL; + rc = ssh_normalize_loose_ip("10.0.1", &result); + assert_int_equal(rc, 0); + assert_non_null(result); + assert_string_equal(result, "10.0.0.1"); + SAFE_FREE(result); + + /* + * The broadcast address "255.255.255.255" is a special case on Windows + * (INADDR_NONE), but on POSIX inet_aton() accepts it and ssh_is_ipaddr() + * already recognises it as a strict address, so it returns rc=1. + */ + result = NULL; + rc = ssh_normalize_loose_ip("255.255.255.255", &result); + assert_int_equal(rc, 1); /* already a strict IPv4 */ + assert_null(result); + + /* Strict dotted-quad — already a valid IP, not "loose" (rc=1) */ + result = NULL; + rc = ssh_normalize_loose_ip("127.0.0.1", &result); + assert_int_equal(rc, 1); + assert_null(result); + + result = NULL; + rc = ssh_normalize_loose_ip("192.168.10.20", &result); + assert_int_equal(rc, 1); + assert_null(result); + + /* IPv6 addresses — not loose IPv4 (rc=1) */ + result = NULL; + rc = ssh_normalize_loose_ip("::1", &result); + assert_int_equal(rc, 1); + assert_null(result); + + result = NULL; + rc = ssh_normalize_loose_ip("2001:db8::1", &result); + assert_int_equal(rc, 1); + assert_null(result); + + /* Hostnames and garbage — not an IP at all (rc=1) */ + result = NULL; + rc = ssh_normalize_loose_ip("example.com", &result); + assert_int_equal(rc, 1); + assert_null(result); + + result = NULL; + rc = ssh_normalize_loose_ip("not-an-ip", &result); + assert_int_equal(rc, 1); + assert_null(result); + + result = NULL; + rc = ssh_normalize_loose_ip("", &result); + assert_int_equal(rc, 1); + assert_null(result); +} + int torture_run_tests(void) { int rc; struct CMUnitTest tests[] = { @@ -1384,6 +1468,7 @@ int torture_run_tests(void) { cmocka_unit_test(torture_ssh_get_hexa), cmocka_unit_test(torture_strlcpy), cmocka_unit_test(torture_strlcat), + cmocka_unit_test(torture_ssh_normalize_loose_ip), }; ssh_init();