Compare commits

...

5 Commits

Author SHA1 Message Date
Arthur Chan
4dfcdd96b8 OSS-Fuzz: Add fuzzer for scp functions
Signed-off-by: Arthur Chan <arthur.chan@adalogics.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-03-13 20:48:53 +01:00
Emmanuel Ugwu
9d36b9dd81 docs: add doxygen documentation and fix inconsistencies
- src/misc.c: added doxygen docs for ssh_get_local_username()
- src/auth.c: added doxygen docs for ssh_kbdint_new(), ssh_kbdint_free(), ssh_kbdint_clean()
- src/bind_config.c: fix @params -> @param, @returns -> @return
- src/bind.c, src/socket.c, src/threads.c: fix @returns -> @return
- include/libssh/callbacks.h: fix @returns -> @return

Signed-off-by: Emmanuel Ugwu <emmanuelugwu121@gmail.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-03-13 20:48:07 +01:00
Rui Li
afa21334b4 tests: Add tests for originalhost/host separation and Match support
Signed-off-by: Rui Li <ruili3422@gmail.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-03-13 20:46:35 +01:00
Rui Li
a2ebc7ea9b Implement originalhost/host separation and Match support
Signed-off-by: Rui Li <ruili3422@gmail.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-03-13 20:46:35 +01:00
Rui Li
1ab8a35c5d Add strict validation mode to ssh_config_parse_uri in config_parser
Signed-off-by: Rui Li <ruili3422@gmail.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-03-13 20:46:35 +01:00
36 changed files with 1121 additions and 65 deletions

View File

@@ -522,7 +522,7 @@ typedef struct ssh_socket_callbacks_struct *ssh_socket_callbacks;
* verifies that the callback pointer exists
* @param p callback pointer
* @param c callback name
* @returns nonzero if callback can be called
* @return nonzero if callback can be called
*/
#define ssh_callbacks_exists(p,c) (\
(p != NULL) && ( (char *)&((p)-> c) < (char *)(p) + (p)->size ) && \

View File

@@ -54,6 +54,11 @@ int ssh_config_get_yesno(char **str, int notfound);
* be stored or NULL if we do not care about the result.
* @param[in] ignore_port Set to true if we should not attempt to parse
* port number.
* @param[in] strict Set to true to validate hostname against RFC1035
* (for resolving to a real host).
* Set to false to only reject shell metacharacters
* (allowing config aliases with non-RFC1035 chars
* like underscores, resolved later via Hostname).
*
* @returns SSH_OK if the provided string is in format of SSH URI,
* SSH_ERROR on failure
@@ -62,7 +67,8 @@ int ssh_config_parse_uri(const char *tok,
char **username,
char **hostname,
char **port,
bool ignore_port);
bool ignore_port,
bool strict);
/**
* @brief: Parse the ProxyJump configuration line and if parsing,

View File

@@ -286,6 +286,9 @@ struct ssh_session_struct {
int control_master;
char *control_path;
int address_family;
char *originalhost; /* user-supplied host for config matching */
bool config_hostname_only; /* config hostname path: update host only,
not originalhost */
} opts;
/* server options */

View File

@@ -1835,6 +1835,14 @@ int ssh_userauth_agent_pubkey(ssh_session session,
return rc;
}
/**
* @internal
*
* @brief Allocates memory for keyboard interactive auth structure.
*
* @return A newly allocated ssh_kbdint structure `kbd` on success, NULL on failure.
* The caller is responsible for freeing allocated memory.
*/
ssh_kbdint ssh_kbdint_new(void)
{
ssh_kbdint kbd;
@@ -1847,7 +1855,11 @@ ssh_kbdint ssh_kbdint_new(void)
return kbd;
}
/**
* @brief Deallocate memory for keyboard interactive auth structure.
*
* @param[in] kbd The keyboard interactive structure to free.
*/
void ssh_kbdint_free(ssh_kbdint kbd)
{
size_t i, n;
@@ -1885,6 +1897,14 @@ void ssh_kbdint_free(ssh_kbdint kbd)
SAFE_FREE(kbd);
}
/**
* @brief Clean a keyboard interactive auth structure.
*
* Clears structure's fields and resets nanswers and nprompts to 0, allowing
* reuse.
*
* @param[in] kbd The keyboard interactive struct to clean
*/
void ssh_kbdint_clean(ssh_kbdint kbd)
{
size_t i, n;

View File

@@ -327,7 +327,7 @@ static int ssh_bind_poll_callback(ssh_poll_handle sshpoll, socket_t fd, int reve
/** @internal
* @brief returns the current poll handle, or creates it
* @param sshbind the ssh_bind object
* @returns a ssh_poll handle suitable for operation
* @return a ssh_poll handle suitable for operation
*/
ssh_poll_handle ssh_bind_get_poll(ssh_bind sshbind)
{

View File

@@ -680,8 +680,8 @@ int ssh_bind_config_parse_file(ssh_bind bind, const char *filename)
* @brief Parse configuration string and set the options to the given bind
* session
*
* @params[in] bind The ssh bind session
* @params[in] input Null terminated string containing the configuration
* @param[in] bind The ssh bind session
* @param[in] input Null terminated string containing the configuration
*
* @warning Options set via this function may be overridden if a configuration
* file is parsed afterwards (e.g., by an implicit call to
@@ -690,7 +690,7 @@ int ssh_bind_config_parse_file(ssh_bind bind, const char *filename)
* It is the callers responsibility to ensure the correct order of
* API calls if explicit options must take precedence.
*
* @returns SSH_OK on successful parsing the configuration string,
* @return SSH_OK on successful parsing the configuration string,
* SSH_ERROR on error
*/
int ssh_bind_config_parse_string(ssh_bind bind, const char *input)

View File

@@ -582,9 +582,8 @@ int ssh_connect(ssh_session session)
session->client = 1;
if (session->opts.fd == SSH_INVALID_SOCKET &&
session->opts.host == NULL &&
session->opts.ProxyCommand == NULL)
{
session->opts.originalhost == NULL &&
session->opts.ProxyCommand == NULL) {
ssh_set_error(session, SSH_FATAL, "Hostname required");
return SSH_ERROR;
}

View File

@@ -544,6 +544,7 @@ ssh_config_parse_proxy_jump(ssh_session session, const char *s, bool do_parsing)
&jump_host->username,
&jump_host->hostname,
&port,
false,
false);
if (rv != SSH_OK) {
ssh_set_error_invalid(session);
@@ -566,7 +567,12 @@ ssh_config_parse_proxy_jump(ssh_session session, const char *s, bool do_parsing)
}
} else if (parse_entry) {
/* We actually care only about the first item */
rv = ssh_config_parse_uri(cp, &username, &hostname, &port, false);
rv = ssh_config_parse_uri(cp,
&username,
&hostname,
&port,
false,
false);
if (rv != SSH_OK) {
ssh_set_error_invalid(session);
goto out;
@@ -582,7 +588,7 @@ ssh_config_parse_proxy_jump(ssh_session session, const char *s, bool do_parsing)
}
} else {
/* The rest is just sanity-checked to avoid failures later */
rv = ssh_config_parse_uri(cp, NULL, NULL, NULL, false);
rv = ssh_config_parse_uri(cp, NULL, NULL, NULL, false, false);
if (rv != SSH_OK) {
ssh_set_error_invalid(session);
goto out;
@@ -1040,20 +1046,20 @@ static int ssh_config_parse_line_internal(ssh_session session,
break;
case MATCH_ORIGINALHOST:
/* Skip one argument */
/* Here we match only one argument */
p = ssh_config_get_str_tok(&s, NULL);
if (p == NULL || p[0] == '\0') {
SSH_LOG(SSH_LOG_TRACE, "line %d: Match keyword "
"'%s' requires argument", count, p2);
ssh_set_error(session,
SSH_FATAL,
"line %d: ERROR - Match originalhost keyword "
"requires argument",
count);
SAFE_FREE(x);
return -1;
}
result &=
ssh_config_match(session->opts.originalhost, p, negate);
args++;
SSH_LOG(SSH_LOG_TRACE,
"line %d: Unsupported Match keyword '%s', ignoring",
count,
p2);
result = 0;
break;
case MATCH_HOST:
@@ -1066,7 +1072,11 @@ static int ssh_config_parse_line_internal(ssh_session session,
SAFE_FREE(x);
return -1;
}
result &= ssh_config_match(session->opts.host, p, negate);
result &= ssh_config_match(session->opts.host
? session->opts.host
: session->opts.originalhost,
p,
negate);
args++;
break;
@@ -1154,7 +1164,9 @@ static int ssh_config_parse_line_internal(ssh_session session,
int ok = 0, result = -1;
*parsing = 0;
lowerhost = (session->opts.host) ? ssh_lowercase(session->opts.host) : NULL;
lowerhost = (session->opts.originalhost)
? ssh_lowercase(session->opts.originalhost)
: NULL;
for (p = ssh_config_get_str_tok(&s, NULL);
p != NULL && p[0] != '\0';
p = ssh_config_get_str_tok(&s, NULL)) {
@@ -1181,7 +1193,9 @@ static int ssh_config_parse_line_internal(ssh_session session,
if (z == NULL) {
z = strdup(p);
}
session->opts.config_hostname_only = true;
ssh_options_set(session, SSH_OPTIONS_HOST, z);
session->opts.config_hostname_only = false;
free(z);
}
break;

View File

@@ -172,7 +172,8 @@ int ssh_config_parse_uri(const char *tok,
char **username,
char **hostname,
char **port,
bool ignore_port)
bool ignore_port,
bool strict)
{
char *endp = NULL;
long port_n;
@@ -243,13 +244,31 @@ int ssh_config_parse_uri(const char *tok,
if (*hostname == NULL) {
goto error;
}
/* if not an ip, check syntax */
rc = ssh_is_ipaddr(*hostname);
if (rc == 0) {
rc = ssh_check_hostname_syntax(*hostname);
if (rc != SSH_OK) {
if (strict) {
/* if not an ip, check syntax */
rc = ssh_is_ipaddr(*hostname);
if (rc == 0) {
rc = ssh_check_hostname_syntax(*hostname);
if (rc != SSH_OK) {
goto error;
}
}
} else {
/* Reject shell metacharacters to allow config aliases with
* non-RFC1035 chars (e.g. %, _). Modeled on OpenSSH's
* valid_hostname() in ssh.c. */
const char *c = NULL;
if ((*hostname)[0] == '-') {
goto error;
}
for (c = *hostname; *c != '\0'; c++) {
char *is_meta = strchr("'`\"$\\;&<>|(){},", *c);
int is_space = isspace((unsigned char)*c);
int is_ctrl = iscntrl((unsigned char)*c);
if (is_meta != NULL || is_space || is_ctrl) {
goto error;
}
}
}
}
/* Skip also the closing bracket */

View File

@@ -208,6 +208,12 @@ struct tm *ssh_localtime(const time_t *timer, struct tm *result)
return result;
}
/**
* @brief Get username from the calling process.
*
* @return An allocated string with the user on success, NULL on failure. The
* caller is responsible for freeing returned string.
*/
char *ssh_get_local_username(void)
{
DWORD size = 0;
@@ -357,6 +363,12 @@ int ssh_dir_writeable(const char *path)
return 0;
}
/**
* @brief Get username from the calling process.
*
* @return An allocated string with the name on success, NULL on failure. The
* caller is responsible for freeing returned string.
*/
char *ssh_get_local_username(void)
{
struct passwd pwd;
@@ -1432,6 +1444,8 @@ char *ssh_path_expand_escape(ssh_session session, const char *s)
case 'h':
if (session->opts.host) {
x = strdup(session->opts.host);
} else if (session->opts.originalhost) {
x = strdup(session->opts.originalhost);
} else {
ssh_set_error(session, SSH_FATAL, "Cannot expand host");
free(buf);

View File

@@ -23,6 +23,7 @@
*/
#include "config.h"
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -106,6 +107,14 @@ int ssh_options_copy(ssh_session src, ssh_session *dest)
}
}
if (src->opts.originalhost != NULL) {
new->opts.originalhost = strdup(src->opts.originalhost);
if (new->opts.originalhost == NULL) {
ssh_free(new);
return -1;
}
}
if (src->opts.bindaddr != NULL) {
new->opts.bindaddr = strdup(src->opts.bindaddr);
if (new->opts.bindaddr == NULL) {
@@ -718,18 +727,52 @@ int ssh_options_set(ssh_session session, enum ssh_options_e type,
return -1;
} else {
char *username = NULL, *hostname = NULL;
rc = ssh_config_parse_uri(value, &username, &hostname, NULL, true);
if (rc != SSH_OK) {
char *strict_hostname = NULL;
/* Non-strict parse: reject shell metacharacters */
rc = ssh_config_parse_uri(value,
&username,
&hostname,
NULL,
true,
false);
if (rc != SSH_OK || hostname == NULL) {
SAFE_FREE(username);
SAFE_FREE(hostname);
ssh_set_error_invalid(session);
return -1;
}
/* Non-strict passed: set username and originalhost */
if (username != NULL) {
SAFE_FREE(session->opts.username);
session->opts.username = username;
}
if (hostname != NULL) {
if (!session->opts.config_hostname_only) {
SAFE_FREE(session->opts.originalhost);
session->opts.originalhost = hostname;
} else {
SAFE_FREE(hostname);
}
/* Strict parse: set host only if valid hostname or IP */
rc = ssh_config_parse_uri(value,
NULL,
&strict_hostname,
NULL,
true,
true);
if (rc != SSH_OK || strict_hostname == NULL) {
SAFE_FREE(session->opts.host);
session->opts.host = hostname;
SAFE_FREE(strict_hostname);
if (session->opts.config_hostname_only) {
/* Config path: Hostname must be valid */
ssh_set_error_invalid(session);
return -1;
}
} else {
SAFE_FREE(session->opts.host);
session->opts.host = strict_hostname;
}
}
break;
@@ -1646,7 +1689,8 @@ int ssh_options_get(ssh_session session, enum ssh_options_e type, char** value)
switch(type)
{
case SSH_OPTIONS_HOST:
src = session->opts.host;
src = session->opts.host ? session->opts.host
: session->opts.originalhost;
break;
case SSH_OPTIONS_USER:
@@ -1980,7 +2024,7 @@ int ssh_options_parse_config(ssh_session session, const char *filename)
if (session == NULL) {
return -1;
}
if (session->opts.host == NULL) {
if (session->opts.originalhost == NULL) {
ssh_set_error_invalid(session);
return -1;
}

View File

@@ -406,6 +406,7 @@ void ssh_free(ssh_session session)
SAFE_FREE(session->opts.bindaddr);
SAFE_FREE(session->opts.username);
SAFE_FREE(session->opts.host);
SAFE_FREE(session->opts.originalhost);
SAFE_FREE(session->opts.homedir);
SAFE_FREE(session->opts.sshdir);
SAFE_FREE(session->opts.knownhosts);

View File

@@ -444,7 +444,8 @@ int ssh_socket_pollcallback(struct ssh_poll_handle_struct *p,
/** @internal
* @brief returns the poll handle corresponding to the socket,
* creates it if it does not exist.
* @returns allocated and initialized ssh_poll_handle object
*
* @return allocated and initialized ssh_poll_handle object
*/
ssh_poll_handle ssh_socket_get_poll_handle(ssh_socket s)
{

View File

@@ -38,7 +38,6 @@ static struct ssh_threads_callbacks_struct *user_callbacks = NULL;
/** @internal
* @brief inits the threading with the backend cryptographic libraries
*/
int ssh_threads_init(void)
{
static int threads_initialized = 0;

View File

@@ -1,8 +1,19 @@
project(fuzzing CXX)
# Build SSH server mock helper as object library
add_library(ssh_server_mock_obj OBJECT ssh_server_mock.c)
target_link_libraries(ssh_server_mock_obj PRIVATE ${TORTURE_LINK_LIBRARIES})
macro(fuzzer name)
add_executable(${name} ${name}.c)
target_link_libraries(${name} PRIVATE ${TORTURE_LINK_LIBRARIES})
# Add ssh_server_mock dependency for scp and sftp fuzzers
if (${name} STREQUAL "ssh_scp_fuzzer" OR ${name} STREQUAL "ssh_sftp_fuzzer")
target_sources(${name} PRIVATE $<TARGET_OBJECTS:ssh_server_mock_obj>)
target_link_libraries(${name} PRIVATE pthread)
endif()
if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
set_target_properties(${name}
PROPERTIES
@@ -36,6 +47,7 @@ fuzzer(ssh_pubkey_fuzzer)
fuzzer(ssh_sftp_attr_fuzzer)
fuzzer(ssh_sshsig_fuzzer)
if (WITH_SERVER)
fuzzer(ssh_scp_fuzzer)
fuzzer(ssh_server_fuzzer)
fuzzer(ssh_bind_config_fuzzer)
endif (WITH_SERVER)

206
tests/fuzz/ssh_scp_fuzzer.c Normal file
View File

@@ -0,0 +1,206 @@
/*
* Copyright 2026 libssh authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <assert.h>
#include <pthread.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#define LIBSSH_STATIC 1
#include <libssh/libssh.h>
#include <libssh/scp.h>
#include "nallocinc.c"
#include "ssh_server_mock.h"
static void _fuzz_finalize(void)
{
ssh_finalize();
}
int LLVMFuzzerInitialize(int *argc, char ***argv)
{
(void)argc;
nalloc_init(*argv[0]);
ssh_init();
atexit(_fuzz_finalize);
ssh_mock_write_hostkey(SSH_MOCK_HOSTKEY_PATH);
return 0;
}
/* Helper function to test one cipher/HMAC combination */
static int test_scp_with_cipher(const uint8_t *data,
size_t size,
const char *cipher,
const char *hmac)
{
int socket_fds[2] = {-1, -1};
ssh_session client_session = NULL;
ssh_scp scp = NULL, scp_recursive = NULL;
char buf[256] = {0};
pthread_t srv_thread;
/* Configure mock SSH server with fuzzer data */
struct ssh_mock_server_config server_config = {
.protocol_data = data,
.protocol_data_size = size,
.exec_callback = ssh_mock_send_raw_data,
.subsystem_callback = NULL,
.callback_userdata = NULL,
.cipher = cipher,
.hmac = hmac,
.server_socket = -1,
.client_socket = -1,
.server_ready = false,
.server_error = false,
};
if (socketpair(AF_UNIX, SOCK_STREAM, 0, socket_fds) != 0) {
goto cleanup;
}
server_config.server_socket = socket_fds[0];
server_config.client_socket = socket_fds[1];
if (ssh_mock_server_start(&server_config, &srv_thread) != 0) {
goto cleanup;
}
client_session = ssh_new();
if (client_session == NULL) {
goto cleanup_thread;
}
/* Configure client with specified cipher/HMAC */
ssh_options_set(client_session, SSH_OPTIONS_FD, &socket_fds[1]);
ssh_options_set(client_session, SSH_OPTIONS_HOST, "localhost");
ssh_options_set(client_session, SSH_OPTIONS_USER, "fuzz");
ssh_options_set(client_session, SSH_OPTIONS_CIPHERS_C_S, cipher);
ssh_options_set(client_session, SSH_OPTIONS_CIPHERS_S_C, cipher);
ssh_options_set(client_session, SSH_OPTIONS_HMAC_C_S, hmac);
ssh_options_set(client_session, SSH_OPTIONS_HMAC_S_C, hmac);
/* Set timeout for operations (1 second) */
long timeout = 1;
ssh_options_set(client_session, SSH_OPTIONS_TIMEOUT, &timeout);
if (ssh_connect(client_session) != SSH_OK) {
goto cleanup_thread;
}
if (ssh_userauth_none(client_session, NULL) != SSH_AUTH_SUCCESS) {
goto cleanup_thread;
}
scp = ssh_scp_new(client_session, SSH_SCP_READ, "/tmp/fuzz");
if (scp == NULL) {
goto cleanup_thread;
}
if (ssh_scp_init(scp) != SSH_OK) {
goto cleanup_thread;
}
if (size > 0) {
size_t copy_size = size < sizeof(buf) ? size : sizeof(buf);
memcpy(buf, data, copy_size);
}
/* Fuzz all SCP API functions in read mode */
ssh_scp_pull_request(scp);
ssh_scp_request_get_filename(scp);
ssh_scp_request_get_permissions(scp);
ssh_scp_request_get_size64(scp);
ssh_scp_request_get_size(scp);
ssh_scp_request_get_warning(scp);
ssh_scp_accept_request(scp);
ssh_scp_deny_request(scp, "Denied by fuzzer");
ssh_scp_read(scp, buf, sizeof(buf));
/* Final fuzz of scp pull request after all the calls */
ssh_scp_pull_request(scp);
/* Fuzz SCP in write/upload + recursive directory mode. */
scp_recursive = ssh_scp_new(client_session,
SSH_SCP_WRITE | SSH_SCP_RECURSIVE,
"/tmp/fuzz-recursive");
if (scp_recursive != NULL) {
if (ssh_scp_init(scp_recursive) == SSH_OK) {
ssh_scp_push_directory(scp_recursive, "fuzz-dir", 0755);
ssh_scp_push_file(scp_recursive, "fuzz-file", sizeof(buf), 0644);
ssh_scp_write(scp_recursive, buf, sizeof(buf));
ssh_scp_leave_directory(scp_recursive);
}
}
cleanup_thread:
pthread_join(srv_thread, NULL);
cleanup:
if (scp_recursive != NULL) {
ssh_scp_close(scp_recursive);
ssh_scp_free(scp_recursive);
}
if (scp) {
ssh_scp_close(scp);
ssh_scp_free(scp);
}
if (client_session) {
ssh_disconnect(client_session);
ssh_free(client_session);
}
if (socket_fds[0] >= 0)
close(socket_fds[0]);
if (socket_fds[1] >= 0)
close(socket_fds[1]);
return 0;
}
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
{
assert(nalloc_start(data, size) > 0);
/* Test all cipher/HMAC combinations exhaustively */
const char *ciphers[] = {
"none",
"aes128-ctr",
"aes256-ctr",
"aes128-cbc",
};
const char *hmacs[] = {
"none",
"hmac-sha1",
"hmac-sha2-256",
};
int num_ciphers = sizeof(ciphers) / sizeof(ciphers[0]);
int num_hmacs = sizeof(hmacs) / sizeof(hmacs[0]);
for (int i = 0; i < num_ciphers; i++) {
for (int j = 0; j < num_hmacs; j++) {
test_scp_with_cipher(data, size, ciphers[i], hmacs[j]);
}
}
nalloc_end();
return 0;
}

View File

@@ -0,0 +1 @@
C0644 50 ../../../etc/passwd

View File

@@ -0,0 +1 @@
C0644 10 dir/file.txt

View File

@@ -0,0 +1 @@
C 100 test

View File

@@ -0,0 +1 @@
C0644 10 ..

View File

@@ -0,0 +1 @@
C0755 1024 executable.sh

View File

@@ -0,0 +1 @@
C0644 999999999999 huge.dat

View File

@@ -0,0 +1 @@
T1234567890 0 1234567890 0

View File

@@ -0,0 +1 @@
C0644 100 test.txt

View File

@@ -0,0 +1 @@
Warning: Test warning

View File

@@ -0,0 +1 @@
C0644 10 .

View File

@@ -0,0 +1 @@
Error: Test error

View File

@@ -0,0 +1 @@
Xunknown command

View File

@@ -0,0 +1 @@
C0644 test

View File

@@ -0,0 +1 @@
D0755 0 mydir

View File

@@ -0,0 +1 @@
C0644 abc test

View File

@@ -0,0 +1,262 @@
/*
* Copyright 2026 libssh authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "ssh_server_mock.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define LIBSSH_STATIC 1
#include <libssh/callbacks.h>
#include <libssh/libssh.h>
#include <libssh/server.h>
/* Fixed ed25519 key for all mock servers */
const char *ssh_mock_ed25519_key_pem =
"-----BEGIN OPENSSH PRIVATE KEY-----\n"
"b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\n"
"QyNTUxOQAAACBpFO8/JfYlIqg6+vqx1vDKWDqxJHxw4tBqnQfiOjf2zAAAAJgbsYq1G7GK\n"
"tQAAAAtzc2gtZWQyNTUxOQAAACBpFO8/JfYlIqg6+vqx1vDKWDqxJHxw4tBqnQfiOjf2zA\n"
"AAAEAkGaLvQwKMbGVRk2M8cz7gqWvpBKuHkuekJxIBQrUJl2kU7z8l9iUiqDr6+rHW8MpY\n"
"OrEkfHDi0GqdB+I6N/bMAAAAEGZ1enotZWQyNTUxOS1rZXkBAgMEBQ==\n"
"-----END OPENSSH PRIVATE KEY-----\n";
/* Internal server session data */
struct mock_session_data {
ssh_channel channel;
struct ssh_mock_server_config *config;
};
/* Auth callback - always accepts "none" auth */
static int mock_auth_none(ssh_session session, const char *user, void *userdata)
{
(void)session;
(void)user;
(void)userdata;
return SSH_AUTH_SUCCESS;
}
/* Channel open callback */
static ssh_channel mock_channel_open(ssh_session session, void *userdata)
{
struct mock_session_data *sdata = (struct mock_session_data *)userdata;
sdata->channel = ssh_channel_new(session);
return sdata->channel;
}
/* Exec request callback - for SCP */
static int mock_channel_exec(ssh_session session,
ssh_channel channel,
const char *command,
void *userdata)
{
struct mock_session_data *sdata = (struct mock_session_data *)userdata;
(void)session;
(void)command;
if (sdata->config->exec_callback) {
return sdata->config->exec_callback(channel,
sdata->config->protocol_data,
sdata->config->protocol_data_size,
sdata->config->callback_userdata);
}
return SSH_OK;
}
/* Subsystem request callback - for SFTP */
static int mock_channel_subsystem(ssh_session session,
ssh_channel channel,
const char *subsystem,
void *userdata)
{
struct mock_session_data *sdata = (struct mock_session_data *)userdata;
(void)session;
(void)subsystem;
if (sdata->config->subsystem_callback) {
return sdata->config->subsystem_callback(
channel,
sdata->config->protocol_data,
sdata->config->protocol_data_size,
sdata->config->callback_userdata);
}
return SSH_OK;
}
/* Server thread implementation */
static void *server_thread_func(void *arg)
{
struct ssh_mock_server_config *config =
(struct ssh_mock_server_config *)arg;
ssh_bind sshbind = NULL;
ssh_session session = NULL;
ssh_event event = NULL;
struct mock_session_data sdata = {0};
sdata.config = config;
struct ssh_server_callbacks_struct server_cb = {
.userdata = &sdata,
.auth_none_function = mock_auth_none,
.channel_open_request_session_function = mock_channel_open,
};
struct ssh_channel_callbacks_struct channel_cb = {
.userdata = &sdata,
.channel_exec_request_function = mock_channel_exec,
.channel_subsystem_request_function = mock_channel_subsystem,
};
bool no = false;
sshbind = ssh_bind_new();
if (sshbind == NULL) {
config->server_error = true;
return NULL;
}
session = ssh_new();
if (session == NULL) {
ssh_bind_free(sshbind);
config->server_error = true;
return NULL;
}
const char *cipher = config->cipher ? config->cipher : "aes128-ctr";
const char *hmac = config->hmac ? config->hmac : "hmac-sha1";
ssh_bind_options_set(sshbind,
SSH_BIND_OPTIONS_HOSTKEY,
SSH_MOCK_HOSTKEY_PATH);
ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_CIPHERS_C_S, cipher);
ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_CIPHERS_S_C, cipher);
ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_HMAC_C_S, hmac);
ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_HMAC_S_C, hmac);
ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_PROCESS_CONFIG, &no);
ssh_set_auth_methods(session, SSH_AUTH_METHOD_NONE);
ssh_callbacks_init(&server_cb);
ssh_set_server_callbacks(session, &server_cb);
if (ssh_bind_accept_fd(sshbind, session, config->server_socket) != SSH_OK) {
ssh_free(session);
ssh_bind_free(sshbind);
config->server_error = true;
return NULL;
}
config->server_ready = true;
event = ssh_event_new();
if (event == NULL) {
ssh_disconnect(session);
ssh_free(session);
ssh_bind_free(sshbind);
return NULL;
}
if (ssh_handle_key_exchange(session) == SSH_OK) {
ssh_event_add_session(event, session);
for (int i = 0; i < 50 && !sdata.channel; i++) {
ssh_event_dopoll(event, 1);
}
if (sdata.channel) {
ssh_callbacks_init(&channel_cb);
ssh_set_channel_callbacks(sdata.channel, &channel_cb);
int max_iterations = 30;
for (int iter = 0; iter < max_iterations &&
!ssh_channel_is_closed(sdata.channel) &&
!ssh_channel_is_eof(sdata.channel);
iter++) {
if (ssh_event_dopoll(event, 100) == SSH_ERROR) {
break;
}
}
}
}
if (event)
ssh_event_free(event);
if (session) {
ssh_disconnect(session);
ssh_free(session);
}
if (sshbind)
ssh_bind_free(sshbind);
return NULL;
}
/* Public API - start mock SSH server */
int ssh_mock_server_start(struct ssh_mock_server_config *config,
pthread_t *thread)
{
if (!config || !thread)
return -1;
config->server_ready = false;
config->server_error = false;
if (pthread_create(thread, NULL, server_thread_func, config) != 0) {
return -1;
}
for (int i = 0; i < 50 && !config->server_ready && !config->server_error;
i++) {
usleep(100);
}
return config->server_error ? -1 : 0;
}
/* Generic protocol callback - sends raw fuzzer data for any protocol */
int ssh_mock_send_raw_data(void *channel,
const void *data,
size_t size,
void *userdata)
{
(void)userdata;
ssh_channel target_channel = (ssh_channel)channel;
/* Send raw fuzzer data - let protocol parser interpret it */
if (size > 0) {
ssh_channel_write(target_channel, data, size);
}
/* Close channel to signal completion */
ssh_channel_send_eof(target_channel);
ssh_channel_close(target_channel);
return SSH_OK;
}
/* Write fixed ed25519 host key to file */
int ssh_mock_write_hostkey(const char *path)
{
FILE *fp = fopen(path, "wb");
if (fp == NULL)
return -1;
size_t len = strlen(ssh_mock_ed25519_key_pem);
size_t nwritten = fwrite(ssh_mock_ed25519_key_pem, 1, len, fp);
fclose(fp);
return (nwritten == len) ? 0 : -1;
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2026 libssh authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef SSH_SERVER_MOCK_H
#define SSH_SERVER_MOCK_H
#include <pthread.h>
#include <stdbool.h>
#include <stdint.h>
/* Server callback type */
typedef int (*ssh_mock_callback_fn)(void *channel,
const void *data,
size_t size,
void *userdata);
/* Mock server configuration */
struct ssh_mock_server_config {
const uint8_t *protocol_data;
size_t protocol_data_size;
ssh_mock_callback_fn exec_callback;
ssh_mock_callback_fn subsystem_callback;
void *callback_userdata;
const char *cipher;
const char *hmac;
int server_socket;
int client_socket;
bool server_ready;
bool server_error;
};
/* Public API functions */
int ssh_mock_server_start(struct ssh_mock_server_config *config,
pthread_t *thread);
int ssh_mock_send_raw_data(void *channel,
const void *data,
size_t size,
void *userdata);
int ssh_mock_write_hostkey(const char *path);
/* Fixed ed25519 key constant */
extern const char *ssh_mock_ed25519_key_pem;
/* Centralized hostkey path used by all mock servers */
#define SSH_MOCK_HOSTKEY_PATH "/tmp/libssh_mock_fuzz_key"
#endif

View File

@@ -130,23 +130,23 @@ extern LIBSSH_THREAD int ssh_log_level;
"ProxyJump = many-spaces.com\n" /* valid */
/* Match keyword */
#define LIBSSH_TESTCONFIG_STRING10 \
"Match host example\n" \
"\tHostName example.com\n" \
"Match host example1,example2\n" \
"\tHostName exampleN\n" \
"Match user guest\n" \
"\tHostName guest.com\n" \
"Match user tester host testhost\n" \
"\tHostName testhost.com\n" \
#define LIBSSH_TESTCONFIG_STRING10 \
"Match host example\n" \
"\tHostName example.com\n" \
"Match host example1,example2\n" \
"\tHostName exampleN\n" \
"Match user guest\n" \
"\tHostName guest.com\n" \
"Match user tester host testhost\n" \
"\tHostName testhost.com\n" \
"Match !user tester host testhost\n" \
"\tHostName nonuser-testhost.com\n" \
"Match all\n" \
"\tHostName all-matched.com\n" \
/* Unsupported options */ \
"Match originalhost example\n" \
"\tHostName original-example.com\n" \
"Match localuser guest\n" \
"\tHostName nonuser-testhost.com\n" \
"Match all\n" \
"\tHostName all-matched.com\n" \
"Match originalhost example\n" \
"\tHostName original-example.com\n" \
"\tUser originaluser\n" \
"Match localuser guest\n" \
"\tHostName local-guest.com\n"
/* ProxyJump */
@@ -851,26 +851,40 @@ static void torture_config_match(void **state,
ssh_options_set(session, SSH_OPTIONS_HOST, "unmatched");
_parse_config(session, file, string, SSH_OK);
assert_string_equal(session->opts.host, "all-matched.com");
assert_string_equal(session->opts.originalhost, "unmatched");
/* Hostname example does simple hostname matching */
torture_reset_config(session);
ssh_options_set(session, SSH_OPTIONS_HOST, "example");
_parse_config(session, file, string, SSH_OK);
assert_string_equal(session->opts.host, "example.com");
assert_string_equal(session->opts.originalhost, "example");
/* We can match also both hosts from a comma separated list */
torture_reset_config(session);
ssh_options_set(session, SSH_OPTIONS_HOST, "example1");
_parse_config(session, file, string, SSH_OK);
assert_string_equal(session->opts.host, "exampleN");
assert_string_equal(session->opts.originalhost, "example1");
torture_reset_config(session);
ssh_options_set(session, SSH_OPTIONS_HOST, "example2");
_parse_config(session, file, string, SSH_OK);
assert_string_equal(session->opts.host, "exampleN");
assert_string_equal(session->opts.originalhost, "example2");
/* We can match by user */
/* We can match by originalhost */
torture_reset_config(session);
ssh_options_set(session, SSH_OPTIONS_HOST, "example");
_parse_config(session, file, string, SSH_OK);
assert_string_equal(session->opts.host, "example.com");
assert_string_equal(session->opts.originalhost, "example");
/* Match originalhost sets User */
assert_string_equal(session->opts.username, "originaluser");
/* We can match by user - clear originalhost to isolate user match */
torture_reset_config(session);
SAFE_FREE(session->opts.originalhost);
ssh_options_set(session, SSH_OPTIONS_USER, "guest");
_parse_config(session, file, string, SSH_OK);
assert_string_equal(session->opts.host, "guest.com");
@@ -881,6 +895,7 @@ static void torture_config_match(void **state,
ssh_options_set(session, SSH_OPTIONS_HOST, "testhost");
_parse_config(session, file, string, SSH_OK);
assert_string_equal(session->opts.host, "testhost.com");
assert_string_equal(session->opts.originalhost, "testhost");
/* We can also negate conditions */
torture_reset_config(session);
@@ -888,9 +903,43 @@ static void torture_config_match(void **state,
ssh_options_set(session, SSH_OPTIONS_HOST, "testhost");
_parse_config(session, file, string, SSH_OK);
assert_string_equal(session->opts.host, "nonuser-testhost.com");
assert_string_equal(session->opts.originalhost, "testhost");
/* In this part, we try various other config files and strings. */
/* Match host compares against resolved hostname */
config = "Host ssh-host\n"
"\tHostname 10.1.1.1\n"
"Match host 10.1.1.*\n"
"\tPort 2222\n";
if (file != NULL) {
torture_write_file(file, config);
} else {
string = config;
}
torture_reset_config(session);
session->opts.port = 0;
ssh_options_set(session, SSH_OPTIONS_HOST, "ssh-host");
_parse_config(session, file, string, SSH_OK);
assert_string_equal(session->opts.host, "10.1.1.1");
assert_string_equal(session->opts.originalhost, "ssh-host");
assert_int_equal(session->opts.port, 2222);
/* Match host falls back to originalhost when host is NULL */
config = "Match host my_alias\n"
"\tHostName alias-matched.com\n";
if (file != NULL) {
torture_write_file(file, config);
} else {
string = config;
}
torture_reset_config(session);
SAFE_FREE(session->opts.username);
ssh_options_set(session, SSH_OPTIONS_HOST, "my_alias");
assert_null(session->opts.host);
_parse_config(session, file, string, SSH_OK);
assert_string_equal(session->opts.host, "alias-matched.com");
/* Match final is not completely supported, but should do quite much the
* same as "match all". The trailing "all" is not mandatory. */
config = "Match final all\n"
@@ -1018,7 +1067,7 @@ static void torture_config_match(void **state,
_parse_config(session, file, string, SSH_OK);
assert_string_equal(session->opts.host, "unmatched");
/* Missing argument to unsupported option originalhost */
/* Missing argument to option originalhost */
config = "Match originalhost\n"
"\tHost originalhost.com\n";
if (file != NULL) {
@@ -1289,7 +1338,6 @@ static void torture_config_proxyjump(void **state,
assert_string_equal(session->opts.ProxyCommand,
"ssh -W '[%h]:%p' 2620:52:0::fed");
/* Multiple @ is allowed in second jump */
config = "Host allowed-hostname\n"
"\tProxyJump localhost,user@principal.com@jumpbox:22\n";
@@ -1351,7 +1399,73 @@ static void torture_config_proxyjump(void **state,
"jumpbox",
"user@principal.com",
"22");
/* Non-RFC-1035 alias (underscore) — accepted with non-strict parse */
config = "Host alias-jump\n"
"\tProxyJump my_alias\n";
if (file != NULL) {
torture_write_file(file, config);
} else {
string = config;
}
torture_reset_config(session);
ssh_options_set(session, SSH_OPTIONS_HOST, "alias-jump");
_parse_config(session, file, string, SSH_OK);
helper_proxy_jump_check(session->opts.proxy_jumps->root,
"my_alias",
NULL,
NULL);
/* Non-RFC-1035 alias in multi-hop second jump */
config = "Host alias-multi\n"
"\tProxyJump localhost,my_alias:2222\n";
if (file != NULL) {
torture_write_file(file, config);
} else {
string = config;
}
torture_reset_config(session);
ssh_options_set(session, SSH_OPTIONS_HOST, "alias-multi");
_parse_config(session, file, string, SSH_OK);
helper_proxy_jump_check(session->opts.proxy_jumps->root,
"my_alias",
NULL,
"2222");
helper_proxy_jump_check(session->opts.proxy_jumps->root->next,
"localhost",
NULL,
NULL);
/* Non-RFC-1035 alias — proxycommand based */
torture_setenv("OPENSSH_PROXYJUMP", "1");
config = "Host alias-jump\n"
"\tProxyJump my_alias\n";
if (file != NULL) {
torture_write_file(file, config);
} else {
string = config;
}
torture_reset_config(session);
ssh_options_set(session, SSH_OPTIONS_HOST, "alias-jump");
_parse_config(session, file, string, SSH_OK);
assert_string_equal(session->opts.ProxyCommand,
"ssh -W '[%h]:%p' my_alias");
/* Non-RFC-1035 alias in multi-hop — proxycommand based */
config = "Host alias-multi\n"
"\tProxyJump localhost,my_alias:2222\n";
if (file != NULL) {
torture_write_file(file, config);
} else {
string = config;
}
torture_reset_config(session);
ssh_options_set(session, SSH_OPTIONS_HOST, "alias-multi");
_parse_config(session, file, string, SSH_OK);
assert_string_equal(session->opts.ProxyCommand,
"ssh -J my_alias:2222 -W '[%h]:%p' localhost");
torture_unsetenv("OPENSSH_PROXYJUMP");
/* In this part, we try various other config files and strings. */
torture_setenv("OPENSSH_PROXYJUMP", "1");
@@ -2762,21 +2876,36 @@ static void torture_config_parse_uri(void **state)
(void)state; /* unused */
rc = ssh_config_parse_uri("localhost", &username, &hostname, &port, false);
rc = ssh_config_parse_uri("localhost",
&username,
&hostname,
&port,
false,
true);
assert_return_code(rc, errno);
assert_null(username);
assert_string_equal(hostname, "localhost");
SAFE_FREE(hostname);
assert_null(port);
rc = ssh_config_parse_uri("1.2.3.4", &username, &hostname, &port, false);
rc = ssh_config_parse_uri("1.2.3.4",
&username,
&hostname,
&port,
false,
true);
assert_return_code(rc, errno);
assert_null(username);
assert_string_equal(hostname, "1.2.3.4");
SAFE_FREE(hostname);
assert_null(port);
rc = ssh_config_parse_uri("1.2.3.4:2222", &username, &hostname, &port, false);
rc = ssh_config_parse_uri("1.2.3.4:2222",
&username,
&hostname,
&port,
false,
true);
assert_return_code(rc, errno);
assert_null(username);
assert_string_equal(hostname, "1.2.3.4");
@@ -2784,7 +2913,12 @@ static void torture_config_parse_uri(void **state)
assert_string_equal(port, "2222");
SAFE_FREE(port);
rc = ssh_config_parse_uri("[1:2:3::4]:2222", &username, &hostname, &port, false);
rc = ssh_config_parse_uri("[1:2:3::4]:2222",
&username,
&hostname,
&port,
false,
true);
assert_return_code(rc, errno);
assert_null(username);
assert_string_equal(hostname, "1:2:3::4");
@@ -2793,13 +2927,64 @@ static void torture_config_parse_uri(void **state)
SAFE_FREE(port);
/* do not want port */
rc = ssh_config_parse_uri("1:2:3::4", &username, &hostname, NULL, true);
rc = ssh_config_parse_uri("1:2:3::4",
&username,
&hostname,
NULL,
true,
true);
assert_return_code(rc, errno);
assert_null(username);
assert_string_equal(hostname, "1:2:3::4");
SAFE_FREE(hostname);
rc = ssh_config_parse_uri("user -name@", &username, NULL, NULL, true);
rc = ssh_config_parse_uri("user -name@", &username, NULL, NULL, true, true);
assert_int_equal(rc, SSH_ERROR);
/* Non-strict accepts non-RFC1035 chars (e.g. _, %) */
rc = ssh_config_parse_uri("customer_1",
&username,
&hostname,
NULL,
true,
false);
assert_return_code(rc, errno);
assert_null(username);
assert_string_equal(hostname, "customer_1");
SAFE_FREE(hostname);
rc = ssh_config_parse_uri("admin@%prod",
&username,
&hostname,
NULL,
true,
false);
assert_return_code(rc, errno);
assert_string_equal(username, "admin");
assert_string_equal(hostname, "%prod");
SAFE_FREE(username);
SAFE_FREE(hostname);
/* Strict rejects what non-strict accepts */
rc = ssh_config_parse_uri("customer_1",
&username,
&hostname,
NULL,
true,
true);
assert_int_equal(rc, SSH_ERROR);
/* Non-strict rejects shell metacharacters */
rc = ssh_config_parse_uri("host;cmd",
&username,
&hostname,
NULL,
true,
false);
assert_int_equal(rc, SSH_ERROR);
/* Non-strict rejects leading dash */
rc = ssh_config_parse_uri("-host", &username, &hostname, NULL, true, false);
assert_int_equal(rc, SSH_ERROR);
}
@@ -2933,6 +3118,48 @@ static void torture_config_jump(void **state)
printf("%s: EOF\n", __func__);
}
/* Verify Hostname directive resolves host without overwriting originalhost
*/
static void torture_config_hostname(void **state)
{
ssh_session session = *state;
char *expanded = NULL;
/* Hostname directive sets host, originalhost is unchanged */
torture_reset_config(session);
ssh_options_set(session, SSH_OPTIONS_HOST, "my_alias");
assert_null(session->opts.host);
assert_string_equal(session->opts.originalhost, "my_alias");
_parse_config(session,
NULL,
"Host my_alias\n\tHostname 192.168.1.1\n",
SSH_OK);
assert_string_equal(session->opts.host, "192.168.1.1");
assert_string_equal(session->opts.originalhost, "my_alias");
/* Host keyword compares against originalhost, not the resolved IP */
torture_reset_config(session);
ssh_options_set(session, SSH_OPTIONS_HOST, "ssh-host");
_parse_config(session,
NULL,
"Host ssh-host\n\tHostname 10.1.1.1\n"
"Host 10.1.1.*\n\tProxyJump ssh-host\n",
SSH_OK);
assert_string_equal(session->opts.host, "10.1.1.1");
assert_string_equal(session->opts.originalhost, "ssh-host");
assert_int_equal(ssh_list_count(session->opts.proxy_jumps), 0);
assert_null(session->opts.ProxyCommand);
/* %h falls back to originalhost when host is not yet resolved */
torture_reset_config(session);
ssh_options_set(session, SSH_OPTIONS_HOST, "my_alias");
assert_null(session->opts.host);
expanded = ssh_path_expand_escape(session, "%h");
assert_non_null(expanded);
assert_string_equal(expanded, "my_alias");
free(expanded);
}
/* Invalid configuration files
*/
static void torture_config_invalid(void **state)
@@ -3101,7 +3328,8 @@ int torture_run_tests(void)
cmocka_unit_test_setup_teardown(torture_config_loglevel_missing_value,
setup,
teardown),
cmocka_unit_test_setup_teardown(torture_config_jump,
cmocka_unit_test_setup_teardown(torture_config_jump, setup, teardown),
cmocka_unit_test_setup_teardown(torture_config_hostname,
setup,
teardown),
cmocka_unit_test_setup_teardown(torture_config_invalid,

View File

@@ -17,6 +17,13 @@
#include <libssh/pki.h>
#include <libssh/pki_priv.h>
#include <libssh/session.h>
#ifdef _WIN32
#include <netioapi.h>
#else
#include <net/if.h>
#endif
#ifdef WITH_SERVER
#include <libssh/bind.h>
#define LIBSSH_CUSTOM_BIND_CONFIG_FILE "my_bind_config"
@@ -59,12 +66,16 @@ static void torture_options_set_host(void **state) {
assert_true(rc == 0);
assert_non_null(session->opts.host);
assert_string_equal(session->opts.host, "localhost");
assert_non_null(session->opts.originalhost);
assert_string_equal(session->opts.originalhost, "localhost");
/* IPv4 address */
rc = ssh_options_set(session, SSH_OPTIONS_HOST, "127.1.1.1");
assert_true(rc == 0);
assert_non_null(session->opts.host);
assert_string_equal(session->opts.host, "127.1.1.1");
assert_non_null(session->opts.originalhost);
assert_string_equal(session->opts.originalhost, "127.1.1.1");
assert_null(session->opts.username);
/* IPv6 address */
@@ -72,12 +83,16 @@ static void torture_options_set_host(void **state) {
assert_true(rc == 0);
assert_non_null(session->opts.host);
assert_string_equal(session->opts.host, "::1");
assert_non_null(session->opts.originalhost);
assert_string_equal(session->opts.originalhost, "::1");
assert_null(session->opts.username);
rc = ssh_options_set(session, SSH_OPTIONS_HOST, "guru@meditation");
assert_true(rc == 0);
assert_non_null(session->opts.host);
assert_string_equal(session->opts.host, "meditation");
assert_non_null(session->opts.originalhost);
assert_string_equal(session->opts.originalhost, "meditation");
assert_non_null(session->opts.username);
assert_string_equal(session->opts.username, "guru");
@@ -86,6 +101,8 @@ static void torture_options_set_host(void **state) {
assert_true(rc == 0);
assert_non_null(session->opts.host);
assert_string_equal(session->opts.host, "hostname");
assert_non_null(session->opts.originalhost);
assert_string_equal(session->opts.originalhost, "hostname");
assert_non_null(session->opts.username);
assert_string_equal(session->opts.username, "at@login");
@@ -104,6 +121,9 @@ static void torture_options_set_host(void **state) {
assert_non_null(session->opts.host);
assert_string_equal(session->opts.host,
"fd4d:5449:7400:111:626d:3cff:fedf:4d39");
assert_non_null(session->opts.originalhost);
assert_string_equal(session->opts.originalhost,
"fd4d:5449:7400:111:626d:3cff:fedf:4d39");
assert_null(session->opts.username);
/* IPv6 hostnames should work also with square braces */
@@ -114,20 +134,103 @@ static void torture_options_set_host(void **state) {
assert_non_null(session->opts.host);
assert_string_equal(session->opts.host,
"fd4d:5449:7400:111:626d:3cff:fedf:4d39");
assert_non_null(session->opts.originalhost);
assert_string_equal(session->opts.originalhost,
"fd4d:5449:7400:111:626d:3cff:fedf:4d39");
assert_null(session->opts.username);
/* user@IPv6%interface
* Use dynamic interface name for cross-platform portability */
{
char interf[IF_NAMESIZE] = {0};
char ipv6_zone[128] = {0};
char expected_host[128] = {0};
if_indextoname(1, interf);
assert_non_null(interf);
snprintf(ipv6_zone, sizeof(ipv6_zone), "user@fe80::1%%%s", interf);
snprintf(expected_host, sizeof(expected_host), "fe80::1%%%s", interf);
rc = ssh_options_set(session, SSH_OPTIONS_HOST, ipv6_zone);
assert_return_code(rc, errno);
assert_non_null(session->opts.host);
assert_string_equal(session->opts.host, expected_host);
assert_non_null(session->opts.originalhost);
assert_string_equal(session->opts.originalhost, expected_host);
assert_string_equal(session->opts.username, "user");
}
/* IDN need to be in punycode format */
SAFE_FREE(session->opts.username);
rc = ssh_options_set(session, SSH_OPTIONS_HOST, "xn--bcher-kva.tld");
assert_return_code(rc, errno);
assert_non_null(session->opts.host);
assert_string_equal(session->opts.host, "xn--bcher-kva.tld");
assert_non_null(session->opts.originalhost);
assert_string_equal(session->opts.originalhost, "xn--bcher-kva.tld");
assert_null(session->opts.username);
/* IDN in UTF8 won't work */
/* IDN in UTF-8 is accepted but not as a valid hostname,
* only originalhost is set */
rc = ssh_options_set(session, SSH_OPTIONS_HOST, "bücher.tld");
assert_string_equal(ssh_get_error(session),
"Invalid argument in ssh_options_set");
assert_return_code(rc, errno);
assert_null(session->opts.host);
assert_non_null(session->opts.originalhost);
assert_string_equal(session->opts.originalhost, "bücher.tld");
/* Config alias '%' rejected by RFC1035, only originalhost is set */
rc = ssh_options_set(session, SSH_OPTIONS_HOST, "%customer1");
assert_return_code(rc, errno);
assert_null(session->opts.host);
assert_non_null(session->opts.originalhost);
assert_string_equal(session->opts.originalhost, "%customer1");
/* user@alias '_' rejected by RFC1035, alias stored in originalhost */
rc = ssh_options_set(session, SSH_OPTIONS_HOST, "admin@customer_1");
assert_return_code(rc, errno);
assert_null(session->opts.host);
assert_non_null(session->opts.originalhost);
assert_string_equal(session->opts.originalhost, "customer_1");
assert_string_equal(session->opts.username, "admin");
/* Shell metacharacters and leading dash rejected.
* Verify failure cases do not update session options. */
SAFE_FREE(session->opts.username);
rc = ssh_options_set(session, SSH_OPTIONS_HOST, "host;rm -rf /");
assert_ssh_return_code_equal(session, rc, SSH_ERROR);
assert_null(session->opts.host);
assert_non_null(session->opts.originalhost);
assert_string_equal(session->opts.originalhost, "customer_1");
assert_null(session->opts.username);
rc = ssh_options_set(session, SSH_OPTIONS_HOST, "-leading-dash");
assert_ssh_return_code_equal(session, rc, SSH_ERROR);
assert_null(session->opts.host);
assert_non_null(session->opts.originalhost);
assert_string_equal(session->opts.originalhost, "customer_1");
assert_null(session->opts.username);
/* Empty user or host rejected */
rc = ssh_options_set(session, SSH_OPTIONS_HOST, "@hostname");
assert_ssh_return_code_equal(session, rc, SSH_ERROR);
assert_null(session->opts.host);
assert_non_null(session->opts.originalhost);
assert_string_equal(session->opts.originalhost, "customer_1");
assert_null(session->opts.username);
rc = ssh_options_set(session, SSH_OPTIONS_HOST, "user@");
assert_ssh_return_code_equal(session, rc, SSH_ERROR);
assert_null(session->opts.host);
assert_non_null(session->opts.originalhost);
assert_string_equal(session->opts.originalhost, "customer_1");
assert_null(session->opts.username);
rc = ssh_options_set(session, SSH_OPTIONS_HOST, "");
assert_ssh_return_code_equal(session, rc, SSH_ERROR);
assert_null(session->opts.host);
assert_non_null(session->opts.originalhost);
assert_string_equal(session->opts.originalhost, "customer_1");
assert_null(session->opts.username);
}
static void torture_options_set_ciphers(void **state)
@@ -714,6 +817,26 @@ static void torture_options_get_host(void **state)
assert_string_equal(host, "localhost");
ssh_string_free_char(host);
/* When host is not yet resolved, falls back to originalhost */
rc = ssh_options_set(session, SSH_OPTIONS_HOST, "my_alias");
assert_true(rc == 0);
assert_null(session->opts.host);
assert_non_null(session->opts.originalhost);
assert_false(ssh_options_get(session, SSH_OPTIONS_HOST, &host));
assert_string_equal(host, "my_alias");
ssh_string_free_char(host);
/* After config resolution, get returns resolved host, not originalhost */
session->opts.host = strdup("192.168.1.1");
assert_non_null(session->opts.host);
assert_false(ssh_options_get(session, SSH_OPTIONS_HOST, &host));
assert_string_equal(host, "192.168.1.1");
ssh_string_free_char(host);
/* originalhost is unchanged */
assert_string_equal(session->opts.originalhost, "my_alias");
}
static void torture_options_set_port(void **state)
@@ -1074,6 +1197,7 @@ static void torture_options_config_host(void **state)
{
ssh_session session = *state;
FILE *config = NULL;
int rv;
/* create a new config file */
config = fopen("test_config", "w");
@@ -1113,6 +1237,33 @@ static void torture_options_config_host(void **state)
ssh_options_parse_config(session, "test_config");
assert_int_equal(session->opts.port, 44);
/* ssh_options_parse_config rejects when originalhost is NULL */
SAFE_FREE(session->opts.host);
SAFE_FREE(session->opts.originalhost);
rv = ssh_options_parse_config(session, "test_config");
assert_int_equal(rv, -1);
/* Config Hostname with invalid hostname: verify stale host not leaked */
torture_write_file("test_config", "Host 192.168.1.1\nHostname my_alias\n");
torture_reset_config(session);
ssh_options_set(session, SSH_OPTIONS_HOST, "192.168.1.1");
assert_string_equal(session->opts.host, "192.168.1.1");
assert_string_equal(session->opts.originalhost, "192.168.1.1");
rv = ssh_options_parse_config(session, "test_config");
assert_int_equal(rv, 0);
assert_null(session->opts.host);
assert_string_equal(session->opts.originalhost, "192.168.1.1");
/* Calling ssh_options_set(HOST) twice: verify stale host not leaked */
ssh_options_set(session, SSH_OPTIONS_HOST, "real.server.com");
assert_string_equal(session->opts.host, "real.server.com");
assert_string_equal(session->opts.originalhost, "real.server.com");
ssh_options_set(session, SSH_OPTIONS_HOST, "my_alias");
assert_null(session->opts.host);
assert_string_equal(session->opts.originalhost, "my_alias");
unlink("test_config");
}
@@ -1437,6 +1588,7 @@ static void torture_options_copy(void **state)
assert_string_equal(session->opts.username, new->opts.username);
assert_string_equal(session->opts.host, new->opts.host);
assert_string_equal(session->opts.originalhost, new->opts.originalhost);
assert_string_equal(session->opts.bindaddr, new->opts.bindaddr);
assert_string_equal(session->opts.sshdir, new->opts.sshdir);
assert_string_equal(session->opts.knownhosts, new->opts.knownhosts);