Compare commits

...

15 Commits

Author SHA1 Message Date
Francesco Rollo
90b07e2c18 refactor(server): Warn about config override behavior in bind APIs
- Add a warning to ssh_bind_listen() clarifying that it implicitly
calls ssh_bind_options_parse_config(), which may override options
previously set via ssh_bind_options_set().

- Add a warning to ssh_bind_options_set() and ssh_bind_config_parse_string()
explaining that options may be overridden if configuration files are parsed
afterwards, either implicitly via ssh_bind_listen() or by an explicit call to
ssh_bind_options_parse_config().

Signed-off-by: Francesco <eferollo@gmail.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-02-24 10:36:59 +01:00
Francesco Rollo
edbd929fa2 feat(server): Add support for -o option argument in server example
Allow passing server configuration options via the -o flag and expose
ssh_bind_config_parse_string() as a public API.

Signed-off-by: Francesco <eferollo@gmail.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-02-24 10:36:58 +01:00
Himaneesh Mishra
38932b74c0 docs: reduce Doxygen warnings in libsshpp.hpp
Signed-off-by: Himaneesh Mishra <himaneeshmishra@gmail.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-02-24 10:35:41 +01:00
Mingyuan Li
60d6179eaa tests: Add opendir handle exhaustion test for sftpserver
Add torture_server_sftp_opendir_handles_exhaustion test that
exercises the error path in process_opendir() when all SFTP
handles are occupied. This covers the memory leak fix for
h->name that was missing in the sftp_handle_alloc() failure path.

The test exhausts all 256 handle slots with sftp_open(), then
verifies that sftp_opendir() fails gracefully without crashing
or leaking memory.

Signed-off-by: Mingyuan Li <2560359315@qq.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-02-24 10:33:36 +01:00
Mingyuan Li
0d9b2c68cc sftpserver: Fix memory leak of h->name in process_opendir error path
When sftp_handle_alloc() fails in process_opendir(), the error path
frees the handle struct h but does not free h->name which was
allocated by strdup(). This causes a memory leak every time the
server runs out of available SFTP handles while processing an
opendir request.

Also add a missing NULL check for the strdup() call itself to
handle out-of-memory conditions gracefully.

This is the same class of bug that was fixed in process_open() by
commit db7f101d (CVE-2025-5449), but was missed in process_opendir().

Signed-off-by: Mingyuan Li <2560359315@qq.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-02-24 10:33:35 +01:00
Madhav Vasisth
adc2462329 docs: clarify ssh-agent API usage and lifecycle
Clarify the session-coupled nature of the ssh-agent interface,
document lifecycle and ownership expectations of agent-related
objects, and describe common error cases and limitations.

No functional changes.

Signed-off-by: Madhav Vasisth <mv2363@srmist.edu.in>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-02-24 10:32:39 +01:00
Pavol Žáčik
0bff33c790 gss-kex: Fix memory leaks in ssh_gssapi_check_client_config
Upon unsuccessful alloc of the gssapi context, the function
would return early without freeing the supported OID set.

With opts->gss_client_identity enabled, the function would
not free the client_id allocated by gss_import_name.

Signed-off-by: Pavol Žáčik <pzacik@redhat.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-02-24 10:30:53 +01:00
Pavol Žáčik
47e9b5536a gss-kex: Release output_token and mic on error paths
Signed-off-by: Pavol Žáčik <pzacik@redhat.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-02-24 10:30:53 +01:00
Pavol Žáčik
2f1f474e27 gssapi: Free both_supported on a new error path
Signed-off-by: Pavol Žáčik <pzacik@redhat.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-02-24 10:30:52 +01:00
Bulitha Kawushika De Zoysa
18d7a3967c tests: Add interoperability tests against TinySSH
This adds a new test suite 'torture_tinyssh' that verifies interoperability with the TinySSH server using various key exchange methods.

Fixes #271

Signed-off-by: Bulitha Kawushika De Zoysa <bulithakaushika99@gmail.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-02-20 10:56:46 +01:00
Antoni Bertolin Monferrer
d45ce10c83 channels: Fix OOM error check after strdup
The fix allows the code to properly check if the strdup failed to allocate a
char buffer for the exit signal.

Signed-off-by: Antoni Bertolin Monferrer <antoni.monferrer@canonical.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-02-20 10:55:17 +01:00
Pavol Žáčik
a7fd80795e Update recently added logging to be less verbose
In 20d9642c and parent commits, log levels were
recategorized to be less verbose when using the
level INFO and lower. These levels should not
print any information redundant to the end user.

This commit fixes recently added uses of logging
that are not consistent with the abovementioned
categorization, in particular:

- logs in ssh_strict_fopen should not have
  the RARE/WARNING level since failing to open
  a file may not be an issue at all (e.g., when
  trying to open the knownhosts file).

- logging the username used in authentication
  or proxyjump-related information should be done
  at the DEBUG level, otherwise it could pollute
  the output of, e.g., curl.

Signed-off-by: Pavol Žáčik <pzacik@redhat.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-02-20 10:54:27 +01:00
Jakub Jelen
f8cba20859 Add back Security section to 0.12.0 changelog
Signed-off-by: Jakub Jelen <jjelen@redhat.com>
Reviewed-by: Pavol Žáčik <pzacik@redhat.com>
2026-02-12 14:54:06 +01:00
Arthur Chan
f13a8d7ced OSS-Fuzz Add fuzzer and corpora for sftp attr parsing
Signed-off-by: Arthur Chan <arthur.chan@adalogics.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-02-10 15:34:39 +01:00
Shreyas Mahajan
c0963b3417 SSH2 NONE authentication
Signed-off-by: Shreyas Mahajan <shreyasmahajan05@gmail.com>
Reviewed-by: Jakub Jelen <jjelen@redhat.com>
2026-02-10 15:16:58 +01:00
39 changed files with 1068 additions and 141 deletions

View File

@@ -2,6 +2,17 @@ CHANGELOG
=========
version 0.12.0 (released 2026-02-10)
* Security:
* CVE-2025-14821: libssh loads configuration files from the C:\etc directory
on Windows
* CVE-2026-0964: SCP Protocol Path Traversal in ssh_scp_pull_request()
* CVE-2026-0965: Possible Denial of Service when parsing unexpected
configuration files
* CVE-2026-0966: Buffer underflow in ssh_get_hexa() on invalid input
* CVE-2026-0967: Specially crafted patterns could cause DoS
* CVE-2026-0968: OOB Read in sftp_parse_longname()
* libssh-2026-sftp-extensions: Read buffer overrun when handling SFTP
extensions
* Deprecations and removals:
* Bumped minimal RSA key size to 1024 bits
* New functionality:

View File

@@ -337,6 +337,7 @@ static void batch_shell(ssh_session session)
static int client(ssh_session session)
{
int auth = 0;
int authenticated = 0;
char *banner = NULL;
int state;
@@ -369,16 +370,28 @@ static int client(ssh_session session)
return -1;
}
ssh_userauth_none(session, NULL);
banner = ssh_get_issue_banner(session);
if (banner) {
printf("%s\n", banner);
free(banner);
}
auth = authenticate_console(session);
if (auth != SSH_AUTH_SUCCESS) {
auth = ssh_userauth_none(session, NULL);
if (auth == SSH_AUTH_SUCCESS) {
authenticated = 1;
} else if (auth == SSH_AUTH_ERROR) {
fprintf(stderr,
"Authentication error during none auth: %s\n",
ssh_get_error(session));
return -1;
}
if (!authenticated) {
auth = authenticate_console(session);
if (auth != SSH_AUTH_SUCCESS) {
return -1;
}
}
if (cmds[0] == NULL) {
shell(session);
} else {

View File

@@ -107,6 +107,14 @@ static struct argp_option options[] = {
.doc = "Set the authorized keys file.",
.group = 0
},
{
.name = "option",
.key = 'o',
.arg = "OPTION",
.flags = 0,
.doc = "Set server configuration option [-o OptionName=Value]",
.group = 0
},
{
.name = "user",
.key = 'u',
@@ -158,6 +166,9 @@ parse_opt(int key, char *arg, struct argp_state *state)
case 'a':
strncpy(authorizedkeys, arg, DEF_STR_SIZE - 1);
break;
case 'o':
ssh_bind_config_parse_string(sshbind, arg);
break;
case 'u':
strncpy(username, arg, sizeof(username) - 1);
break;
@@ -194,7 +205,7 @@ parse_opt(int argc, char **argv, ssh_bind sshbind)
{
int key;
while((key = getopt(argc, argv, "a:e:k:p:P:r:u:v")) != -1) {
while((key = getopt(argc, argv, "a:e:k:o:p:P:r:u:v")) != -1) {
if (key == 'p') {
ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_BINDPORT_STR, optarg);
} else if (key == 'k') {
@@ -205,6 +216,8 @@ parse_opt(int argc, char **argv, ssh_bind sshbind)
ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_HOSTKEY, optarg);
} else if (key == 'a') {
strncpy(authorizedkeys, optarg, DEF_STR_SIZE-1);
} else if (key == 'o') {
ssh_bind_config_parse_string(sshbind, optarg);
} else if (key == 'u') {
strncpy(username, optarg, sizeof(username) - 1);
} else if (key == 'P') {
@@ -222,6 +235,7 @@ parse_opt(int argc, char **argv, ssh_bind sshbind)
"libssh %s -- a Secure Shell protocol implementation\n"
"\n"
" -a, --authorizedkeys=FILE Set the authorized keys file.\n"
" -o, --option=OPTION Set server configuration option (e.g., -o OptionName=Value).\n"
" -e, --ecdsakey=FILE Set the ecdsa key (deprecated alias for 'k').\n"
" -k, --hostkey=FILE Set a host key. Can be used multiple times.\n"
" Implies no default keys.\n"

View File

@@ -74,6 +74,13 @@
extern "C" {
#endif
/**
* @brief SSH agent connection context.
*
* This structure represents a connection to an SSH authentication agent.
* It is used by libssh to communicate with the agent for operations such
* as listing available identities and signing data during authentication.
*/
struct ssh_agent_struct {
struct ssh_socket_struct *sock;
ssh_buffer ident;
@@ -82,13 +89,24 @@ struct ssh_agent_struct {
};
/* agent.c */
/**
* @brief Create a new ssh agent structure.
*
* @return An allocated ssh agent structure or NULL on error.
* Creates and initializes an SSH agent context bound to the given
* SSH session. The agent connection relies on the session configuration
* (e.g. agent forwarding).
*
* @param session The SSH session the agent will be associated with.
*
* @return An allocated ssh agent structure on success, or NULL on error.
*
* @note This function does not start or manage an external agent
* process; it only connects to an already running agent.
*/
struct ssh_agent_struct *ssh_agent_new(struct ssh_session_struct *session);
void ssh_agent_close(struct ssh_agent_struct *agent);
/**
@@ -101,24 +119,65 @@ void ssh_agent_free(struct ssh_agent_struct *agent);
/**
* @brief Check if the ssh agent is running.
*
* @param session The ssh session to check for the agent.
* @param session The SSH session to check for agent availability.
*
* @return 1 if it is running, 0 if not.
* @return 1 if an agent is available, 0 otherwise.
*/
int ssh_agent_is_running(struct ssh_session_struct *session);
uint32_t ssh_agent_get_ident_count(struct ssh_session_struct *session);
ssh_key ssh_agent_get_next_ident(struct ssh_session_struct *session,
char **comment);
/**
* @brief Retrieve the first identity provided by the SSH agent.
*
* @param session The SSH session associated with the agent.
* @param comment Optional pointer to receive the key comment.
*
* @return A public key on success, or NULL if no identities are available.
*
* @note The returned key is owned by the caller and must be freed
* using ssh_key_free().
*/
ssh_key ssh_agent_get_first_ident(struct ssh_session_struct *session,
char **comment);
/**
* @brief Retrieve the next identity provided by the SSH agent.
*
* @param session The SSH session associated with the agent.
* @param comment Optional pointer to receive the key comment.
*
* @return A public key on success, or NULL if no further identities exist.
*
* @note The returned key is owned by the caller and must be freed
* using ssh_key_free().
*/
ssh_key ssh_agent_get_next_ident(struct ssh_session_struct *session,
char **comment);
/**
* @brief Request the SSH agent to sign data using a public key.
*
* Asks the SSH agent to generate a signature over the provided data
* using the specified public key.
*
* @param session The SSH session associated with the agent.
* @param pubkey The public key identifying the signing identity.
* @param data The data to be signed.
*
* @return A newly allocated ssh_string containing the signature on
* success, or NULL on failure.
*
* @note This operation requires that the agent possesses the
* corresponding private key and may prompt the user for
* confirmation depending on agent configuration.
*/
ssh_string ssh_agent_sign_data(ssh_session session,
const ssh_key pubkey,
struct ssh_buffer_struct *data);
#ifdef __cplusplus
}
#endif

View File

@@ -66,16 +66,6 @@ enum ssh_bind_config_opcode_e {
*/
int ssh_bind_config_parse_file(ssh_bind sshbind, 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
*
* @returns SSH_OK on successful parsing the configuration string,
* SSH_ERROR on error
*/
int ssh_bind_config_parse_string(ssh_bind bind, const char *input);
#ifdef __cplusplus
}
#endif

View File

@@ -67,38 +67,60 @@ class Channel;
*/
#ifndef SSH_NO_CPP_EXCEPTIONS
/** @brief This class describes a SSH Exception object. This object can be thrown
* by several SSH functions that interact with the network, and may fail because of
* socket, protocol or memory errors.
/** @brief This class describes a SSH Exception object. This object can be
* thrown by several SSH functions that interact with the network, and
* may fail because of socket, protocol or memory errors.
*/
class SshException{
public:
SshException(ssh_session csession){
code=ssh_get_error_code(csession);
description=std::string(ssh_get_error(csession));
}
SshException(const SshException &e){
code=e.code;
description=e.description;
}
/** @brief returns the Error code
* @returns SSH_FATAL Fatal error happened (not recoverable)
* @returns SSH_REQUEST_DENIED Request was denied by remote host
* @see ssh_get_error_code
*/
int getCode(){
return code;
}
/** @brief returns the error message of the last exception
* @returns pointer to a c string containing the description of error
* @see ssh_get_error
*/
std::string getError(){
return description;
}
private:
int code;
std::string description;
class SshException {
public:
/** @brief Construct an exception from a libssh session error state.
* Captures the current error code and error string associated with
* the given session.
*
* @param[in] csession libssh session handle used to query error details.
*
* @see ssh_get_error_code
* @see ssh_get_error
*/
SshException(ssh_session csession)
{
code = ssh_get_error_code(csession);
description = std::string(ssh_get_error(csession));
}
/** @brief Copy-construct an exception.
*
* @param[in] e Source exception.
*/
SshException(const SshException &e)
{
code = e.code;
description = e.description;
}
/** @brief returns the Error code
*
* @returns `SSH_FATAL` Fatal error happened (not recoverable)
* @returns `SSH_REQUEST_DENIED` Request was denied by remote host
*
* @see ssh_get_error_code
*/
int getCode()
{
return code;
}
/** @brief returns the error message of the last exception
*
* @returns pointer to a c string containing the description of error
*
* @see ssh_get_error
*/
std::string getError()
{
return description;
}
private:
int code;
std::string description;
};
/** @internal
@@ -134,9 +156,12 @@ public:
c_session=NULL;
}
/** @brief sets an SSH session options
* @param type Type of option
* @param option cstring containing the value of option
*
* @param[in] type Type of option
* @param[in] option cstring containing the value of option
*
* @throws SshException on error
*
* @see ssh_options_set
*/
void_throwable setOption(enum ssh_options_e type, const char *option){
@@ -144,9 +169,12 @@ public:
return_throwable;
}
/** @brief sets an SSH session options
* @param type Type of option
* @param option long integer containing the value of option
*
* @param[in] type Type of option
* @param[in] option long integer containing the value of option
*
* @throws SshException on error
*
* @see ssh_options_set
*/
void_throwable setOption(enum ssh_options_e type, long int option){
@@ -154,9 +182,12 @@ public:
return_throwable;
}
/** @brief sets an SSH session options
* @param type Type of option
* @param option void pointer containing the value of option
*
* @param[in] type Type of option
* @param[in] option void pointer containing the value of option
*
* @throws SshException on error
*
* @see ssh_options_set
*/
void_throwable setOption(enum ssh_options_e type, void *option){
@@ -164,7 +195,9 @@ public:
return_throwable;
}
/** @brief connects to the remote host
*
* @throws SshException on error
*
* @see ssh_connect
*/
void_throwable connect(){
@@ -173,8 +206,11 @@ public:
return_throwable;
}
/** @brief Authenticates automatically using public key
*
* @throws SshException on error
* @returns SSH_AUTH_SUCCESS, SSH_AUTH_PARTIAL, SSH_AUTH_DENIED
*
* @returns `SSH_AUTH_SUCCESS`, `SSH_AUTH_PARTIAL`, `SSH_AUTH_DENIED`
*
* @see ssh_userauth_autopubkey
*/
int userauthPublickeyAuto(void){
@@ -184,9 +220,13 @@ public:
}
/** @brief Authenticates using the "none" method. Prefer using autopubkey if
* possible.
*
* @throws SshException on error
* @returns SSH_AUTH_SUCCESS, SSH_AUTH_PARTIAL, SSH_AUTH_DENIED
*
* @returns `SSH_AUTH_SUCCESS`, `SSH_AUTH_PARTIAL`, `SSH_AUTH_DENIED`
*
* @see ssh_userauth_none
*
* @see Session::userauthAutoPubkey
*/
int userauthNone(){
@@ -198,16 +238,16 @@ public:
/**
* @brief Authenticate through the "keyboard-interactive" method.
*
* @param[in] username The username to authenticate. You can specify NULL if
* ssh_option_set_username() has been used. You cannot
* try two different logins in a row.
*
* @param[in] username The username to authenticate. You can specify NULL
* If ssh_option_set_username()has been used. You cannot
* try two different logins in a row.
* @param[in] submethods Undocumented. Set it to NULL.
*
* @throws SshException on error
*
* @returns SSH_AUTH_SUCCESS, SSH_AUTH_PARTIAL, SSH_AUTH_DENIED,
* SSH_AUTH_ERROR, SSH_AUTH_INFO, SSH_AUTH_AGAIN
* @returns `SSH_AUTH_SUCCESS`, `SSH_AUTH_PARTIAL`,
* `SSH_AUTH_DENIED`, `SSH_AUTH_ERROR`, `SSH_AUTH_INFO`,
* `SSH_AUTH_AGAIN`
*
* @see ssh_userauth_kbdint
*/
@@ -218,6 +258,7 @@ public:
}
/** @brief Get the number of prompts (questions) the server has given.
*
* @returns The number of prompts.
* @see ssh_userauth_kbdint_getnprompts
*/
@@ -228,17 +269,16 @@ public:
/**
* @brief Set the answer for a question from a message block.
*
* @param[in] index The index number of the prompt.
* @param[in] answer The answer to give to the server. The answer MUST be
* encoded UTF-8. It is up to the server how to interpret
* the value and validate it. However, if you read the
* answer in some other encoding, you MUST convert it to
* UTF-8.
* @param[in] index The index number of the prompt.
* @param[in] answer The answer to give to the server. The answer MUST be
* encoded UTF-8.It is up to the server how to interpret
* the value and validate it. However, if you read the
* answer in some other encoding, you MUST convert it to
* UTF-8.
*
* @throws SshException on error
*
* @returns 0 on success, < 0 on error
*
* @see ssh_userauth_kbdint_setanswer
*/
int userauthKbdintSetAnswer(unsigned int index, const char *answer)
@@ -248,12 +288,13 @@ public:
return ret;
}
/** @brief Authenticates using the password method.
*
* @param[in] password password to use for authentication
*
* @throws SshException on error
* @returns SSH_AUTH_SUCCESS, SSH_AUTH_PARTIAL, SSH_AUTH_DENIED
* @returns `SSH_AUTH_SUCCESS`, `SSH_AUTH_PARTIAL`, `SSH_AUTH_DENIED`
*
* @see ssh_userauth_password
*/
int userauthPassword(const char *password){
@@ -262,10 +303,14 @@ public:
return ret;
}
/** @brief Try to authenticate using the publickey method.
*
* @param[in] pubkey public key to use for authentication
*
* @throws SshException on error
* @returns SSH_AUTH_SUCCESS if the pubkey is accepted,
* @returns SSH_AUTH_DENIED if the pubkey is denied
*
* @returns `SSH_AUTH_SUCCESS` if the pubkey is accepted,
* @returns `SSH_AUTH_DENIED` if the pubkey is denied
*
* @see ssh_userauth_try_pubkey
*/
int userauthTryPublickey(ssh_key pubkey){
@@ -274,9 +319,12 @@ public:
return ret;
}
/** @brief Authenticates using the publickey method.
*
* @param[in] privkey private key to use for authentication
*
* @throws SshException on error
* @returns SSH_AUTH_SUCCESS, SSH_AUTH_PARTIAL, SSH_AUTH_DENIED
* @returns `SSH_AUTH_SUCCESS`, `SSH_AUTH_PARTIAL`, `SSH_AUTH_DENIED`
*
* @see ssh_userauth_pubkey
*/
int userauthPublickey(ssh_key privkey){
@@ -286,7 +334,9 @@ public:
}
/** @brief Returns the available authentication methods from the server
*
* @throws SshException on error
*
* @returns Bitfield of available methods.
* @see ssh_userauth_list
*/
@@ -302,8 +352,9 @@ public:
ssh_disconnect(c_session);
}
/** @brief Returns the disconnect message from the server, if any
* @returns pointer to the message, or NULL. Do not attempt to free
* the pointer.
*
* @returns pointer to the message, or NULL. Do not attempt to free the
* pointer.
*/
const char *getDisconnectMessage(){
const char *msg=ssh_get_disconnect_message(c_session);
@@ -312,25 +363,30 @@ public:
/** @internal
* @brief gets error message
*/
const char *getError(){
return ssh_get_error(c_session);
const char *getError()
{
return ssh_get_error(c_session);
}
/** @internal
* @brief returns error code
*/
int getErrorCode(){
return ssh_get_error_code(c_session);
int getErrorCode()
{
return ssh_get_error_code(c_session);
}
/** @brief returns the file descriptor used for the communication
*
* @returns the file descriptor
*
* @warning if a proxycommand is used, this function will only return
* one of the two file descriptors being used
* one of the two file descriptors being used.
* @see ssh_get_fd
*/
socket_t getSocket(){
return ssh_get_fd(c_session);
}
/** @brief gets the Issue banner from the ssh server
*
* @returns the issue banner. This is generally a MOTD from server
* @see ssh_get_issue_banner
*/
@@ -344,6 +400,7 @@ public:
return ret;
}
/** @brief returns the OpenSSH version (server) if possible
*
* @returns openssh version code
* @see ssh_get_openssh_version
*/
@@ -351,6 +408,7 @@ public:
return ssh_get_openssh_version(c_session);
}
/** @brief returns the version of the SSH protocol being used
*
* @returns the SSH protocol version
* @see ssh_get_version
*/
@@ -358,9 +416,10 @@ public:
return ssh_get_version(c_session);
}
/** @brief verifies that the server is known
*
* @throws SshException on error
* @returns Integer value depending on the knowledge of the
* server key
*
* @returns Integer value depending on the knowledge of the server key
* @see ssh_session_update_known_hosts
*/
int isServerKnown(){
@@ -377,6 +436,7 @@ public:
}
/** @brief copies options from a session to another
*
* @throws SshException on error
* @see ssh_options_copy
*/
@@ -385,8 +445,11 @@ public:
return_throwable;
}
/** @brief parses a configuration file for options
*
* @throws SshException on error
*
* @param[in] file configuration file name
*
* @see ssh_options_parse_config
*/
void_throwable optionsParseConfig(const char *file){
@@ -399,8 +462,8 @@ public:
void silentDisconnect(){
ssh_silent_disconnect(c_session);
}
/** @brief Writes the known host file with current
* host key
/** @brief Writes the known host file with current host key
*
* @throws SshException on error
* @see ssh_write_knownhost
*/
@@ -411,11 +474,15 @@ public:
}
/** @brief accept an incoming forward connection
*
* @param[in] timeout_ms timeout for waiting, in ms
*
* @returns new Channel pointer on the forward connection
* @returns NULL in case of error
*
* @warning you have to delete this pointer after use
* @see ssh_channel_forward_accept
*
* @see Session::listenForward
*/
inline Channel *acceptForward(int timeout_ms);
@@ -439,6 +506,9 @@ public:
}
protected:
/** @internal
* @brief Underlying libssh session handle.
*/
ssh_session c_session;
private:
@@ -447,8 +517,7 @@ private:
Session& operator=(const Session &);
};
/** @brief the ssh::Channel class describes the state of an SSH
* channel.
/** @brief the ssh::Channel class describes the state of an SSH channel.
* @see ssh_channel
*/
class Channel {
@@ -464,11 +533,15 @@ public:
}
/** @brief accept an incoming X11 connection
*
* @param[in] timeout_ms timeout for waiting, in ms
*
* @returns new Channel pointer on the X11 connection
* @returns NULL in case of error
*
* @warning you have to delete this pointer after use
* @see ssh_channel_accept_x11
*
* @see Channel::requestX11
*/
Channel *acceptX11(int timeout_ms){
@@ -478,8 +551,10 @@ public:
return newchan;
}
/** @brief change the size of a pseudoterminal
*
* @param[in] cols number of columns
* @param[in] rows number of rows
*
* @throws SshException on error
* @see ssh_channel_change_pty_size
*/
@@ -490,6 +565,7 @@ public:
}
/** @brief closes a channel
*
* @throws SshException on error
* @see ssh_channel_close
*/
@@ -536,6 +612,21 @@ public:
bool isOpen(){
return ssh_channel_is_open(channel) != 0;
}
/** @brief Open a TCP forward channel.
*
* @param[in] remotehost Remote host to connect to.
* @param[in] remoteport Remote port to connect to.
* @param[in] sourcehost Source address to report (can be NULL depending on
* server policy).
* @param[in] localport Local port to report (0 lets the server pick if
* applicable).
*
* @returns `SSH_OK` (0) on success.
* @returns `SSH_ERROR` on error (no-exception builds).
*
* @throws SshException on error (exception-enabled builds).
* @see ssh_channel_open_forward
*/
int openForward(const char *remotehost, int remoteport,
const char *sourcehost, int localport=0){
int err=ssh_channel_open_forward(channel,remotehost,remoteport,
@@ -549,20 +640,55 @@ public:
ssh_throw(err);
return_throwable;
}
int poll(bool is_stderr=false){
int err=ssh_channel_poll(channel,is_stderr);
ssh_throw(err);
return err;
/** @brief Poll the channel for available data.
*
* @param[in] is_stderr If true, poll stderr stream; otherwise stdout.
*
* @returns Number of bytes available to read (>= 0).
* @returns `SSH_ERROR` on error (no-exception builds).
*
* @throws SshException on error (exception-enabled builds).
* @see ssh_channel_poll
*/
int poll(bool is_stderr = false)
{
int err = ssh_channel_poll(channel, is_stderr);
ssh_throw(err);
return err;
}
int read(void *dest, size_t count){
int err;
/* handle int overflow */
if(count > 0x7fffffff)
count = 0x7fffffff;
err=ssh_channel_read_timeout(channel,dest,count,false,-1);
ssh_throw(err);
return err;
/** @brief Read data from the channel (blocking).
*
* @param[out] dest Destination buffer.
* @param[in] count Maximum number of bytes to read.
*
* @returns Number of bytes read (>= 0). A return of 0 indicates EOF/no data.
* @returns `SSH_ERROR` on error (no-exception builds).
*
* @throws SshException on error (exception-enabled builds).
* @see ssh_channel_read_timeout
*/
int read(void *dest, size_t count)
{
int err;
if (count > 0x7fffffff)
count = 0x7fffffff;
err = ssh_channel_read_timeout(channel, dest, count, false, -1);
ssh_throw(err);
return err;
}
/** @brief Read data from the channel with a timeout.
*
* @param[out] dest Destination buffer.
* @param[in] count Maximum number of bytes to read.
* @param[in] timeout Timeout in milliseconds. A negative value means
* infinite timeout.
*
* @returns Number of bytes read (>= 0). A return value of 0 indicates EOF.
* @returns `SSH_ERROR` on error (no-exception builds).
*
* @throws SshException on error (exception-enabled builds).
* @see ssh_channel_read_timeout
*/
int read(void *dest, size_t count, int timeout){
int err;
/* handle int overflow */
@@ -572,6 +698,22 @@ public:
ssh_throw(err);
return err;
}
/** @brief Read data from the channel with optional stderr selection and
* timeout.
*
* @param[out] dest Destination buffer.
* @param[in] count Maximum number of bytes to read.
* @param[in] is_stderr If true, read from the stderr stream; otherwise
* read from stdout.
* @param[in] timeout Timeout in milliseconds. A negative value means
* infinite timeout.
*
* @returns Number of bytes read (>= 0). A return value of 0 indicates EOF.
* @returns `SSH_ERROR` on error (no-exception builds).
*
* @throws SshException on error (exception-enabled builds).
* @see ssh_channel_read_timeout
*/
int read(void *dest, size_t count, bool is_stderr=false, int timeout=-1){
int err;
/* handle int overflow */
@@ -581,6 +723,19 @@ public:
ssh_throw(err);
return err;
}
/** @brief Read data from the channel without blocking.
*
* @param[out] dest Destination buffer.
* @param[in] count Maximum number of bytes to read.
* @param[in] is_stderr If true, read from the stderr stream; otherwise read
* from stdout.
*
* @returns Number of bytes read (>= 0). A return of 0 may indicate no data.
* @returns `SSH_ERROR` on error (no-exception builds).
*
* @throws SshException on error (exception-enabled builds).
* @see ssh_channel_read_nonblocking
*/
int readNonblocking(void *dest, size_t count, bool is_stderr=false){
int err;
/* handle int overflow */
@@ -629,6 +784,18 @@ public:
ssh_throw(err);
return_throwable;
}
/** @brief Request X11 forwarding for this channel.
*
* @param[in] single_connection If true, allow only a single X11 connection
* for this channel; further X11 connections are
* refused after the first is accepted.
* @param[in] protocol X11 authentication protocol.
* @param[in] cookie X11 authentication cookie.
* @param[in] screen_number X11 screen number.
*
* @returns `SSH_OK` on success.
* @returns `SSH_ERROR` on error (no-exception builds).
*/
int requestX11(bool single_connection,
const char *protocol, const char *cookie, int screen_number){
int err=ssh_channel_request_x11(channel,single_connection,
@@ -641,11 +808,16 @@ public:
ssh_throw(err);
return_throwable;
}
/** @brief Writes on a channel
* @param data data to write.
* @param len number of bytes to write.
* @param is_stderr write should be done on the stderr channel (server only)
/**
* @brief Writes on a channel
*
* @param[in] data data to write.
* @param[in] len number of bytes to write.
* @param[in] is_stderr write should be done on the stderr channel (server
* only)
*
* @returns number of bytes written
*
* @throws SshException in case of error
* @see ssh_channel_write
* @see ssh_channel_write_stderr
@@ -670,7 +842,13 @@ public:
}
protected:
/** @internal
* @brief Parent session owning this channel.
*/
Session *session;
/** @internal
* @brief Underlying libssh channel handle.
*/
ssh_channel channel;
private:
@@ -683,7 +861,6 @@ private:
Channel &operator=(const Channel &);
};
inline Channel *Session::acceptForward(int timeout_ms){
ssh_channel forward =
ssh_channel_open_forward_port(c_session, timeout_ms, NULL, NULL, NULL);

View File

@@ -102,12 +102,31 @@ LIBSSH_API int ssh_bind_options_set(ssh_bind sshbind,
LIBSSH_API int ssh_bind_options_parse_config(ssh_bind sshbind,
const char *filename);
LIBSSH_API int ssh_bind_config_parse_string(ssh_bind bind, const char *input);
/**
* @brief Start listening to the socket.
*
* @param ssh_bind_o The ssh server bind to use.
*
* @return 0 on success, < 0 on error.
*
* @warning This function implicitly calls ssh_bind_options_parse_config()
* to process system-wide and user configuration files unless
* configuration processing was already performed explicitly
* by the caller.\n
* As a result, any options previously set (e.g., manually via
* ssh_bind_options_set() or ssh_bind_config_parse_string()) may be
* overridden by values from the configuration files.\n
* To guarantee that explicitly set options take precedence,
* callers of this function should either:
* - call ssh_bind_options_parse_config() themselves before
* setting options, or
* - disable automatic config processing via
* SSH_BIND_OPTIONS_PROCESS_CONFIG (set to false).
*
* @see ssh_bind_options_parse_config()
* @see ssh_bind_options_set()
*/
LIBSSH_API int ssh_bind_listen(ssh_bind ssh_bind_o);

View File

@@ -1397,7 +1397,7 @@ int ssh_userauth_publickey_auto(ssh_session session,
return SSH_AUTH_ERROR;
}
SSH_LOG(SSH_LOG_INFO,
SSH_LOG(SSH_LOG_DEBUG,
"Starting authentication as a user %s",
username ? username : session->opts.username);

View File

@@ -676,11 +676,20 @@ int ssh_bind_config_parse_file(ssh_bind bind, const char *filename)
return 0;
}
/* @brief Parse configuration string and set the options to the given bind session
/**
* @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
*
* @warning Options set via this function may be overridden if a configuration
* file is parsed afterwards (e.g., by an implicit call to
* ssh_bind_options_parse_config() inside ssh_bind_listen(), or by a
* manual call to the same function) and contains the same options.\n
* 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,
* SSH_ERROR on error
*/
@@ -713,21 +722,29 @@ int ssh_bind_config_parse_string(ssh_bind bind, const char *input)
}
if (c == NULL) {
/* should not happen, would mean a string without trailing '\0' */
SSH_LOG(SSH_LOG_WARN, "No trailing '\\0' in config string");
ssh_set_error(bind,
SSH_FATAL,
"No trailing '\\0' in config string");
return SSH_ERROR;
}
line_len = c - line_start;
if (line_len > MAX_LINE_SIZE - 1) {
SSH_LOG(SSH_LOG_WARN,
"Line %u too long: %zu characters",
line_num,
line_len);
ssh_set_error(bind,
SSH_FATAL,
"Line %u too long: %zu characters",
line_num,
line_len);
return SSH_ERROR;
}
memcpy(line, line_start, line_len);
line[line_len] = '\0';
SSH_LOG(SSH_LOG_DEBUG, "Line %u: %s", line_num, line);
rv = ssh_bind_config_parse_line(bind, line, line_num, &parser_flags, seen, 0);
rv = ssh_bind_config_parse_line(bind,
line,
line_num,
&parser_flags,
seen,
0);
if (rv < 0) {
return SSH_ERROR;
}

View File

@@ -3517,7 +3517,7 @@ int ssh_channel_get_exit_state(ssh_channel channel,
*pexit_signal = NULL;
if (channel->exit.signal != NULL) {
*pexit_signal = strdup(channel->exit.signal);
if (pexit_signal == NULL) {
if (*pexit_signal == NULL) {
ssh_set_error_oom(session);
return SSH_ERROR;
}

View File

@@ -258,7 +258,9 @@ local_parse_file(ssh_session session,
f = ssh_strict_fopen(filename, SSH_MAX_CONFIG_FILE_SIZE);
if (f == NULL) {
/* The underlying function logs the reasons */
SSH_LOG(SSH_LOG_RARE,
"Failed to open included configuration file %s",
filename);
return;
}

View File

@@ -243,6 +243,7 @@ ssh_gssapi_handle_userauth(ssh_session session, const char *user,
/* Get the server supported oids */
rc = ssh_gssapi_server_oids(&supported);
if (rc != SSH_OK) {
gss_release_oid_set(&min_stat, &both_supported);
return SSH_ERROR;
}
@@ -730,7 +731,8 @@ int ssh_gssapi_check_client_config(ssh_session session)
gssapi = calloc(1, sizeof(struct ssh_gssapi_struct));
if (gssapi == NULL) {
ssh_set_error_oom(session);
return SSH_ERROR;
ret = SSH_ERROR;
break;
}
gssapi->server_creds = GSS_C_NO_CREDENTIAL;
gssapi->client_creds = GSS_C_NO_CREDENTIAL;
@@ -819,6 +821,11 @@ int ssh_gssapi_check_client_config(ssh_session session)
gss_release_buffer(&min_stat, &output_token);
gss_delete_sec_context(&min_stat, &gssapi->ctx, GSS_C_NO_BUFFER);
if (client_id != GSS_C_NO_NAME) {
gss_release_name(&min_stat, &client_id);
client_id = GSS_C_NO_NAME;
}
SAFE_FREE(gssapi->canonic_user);
SAFE_FREE(gssapi);

View File

@@ -591,6 +591,7 @@ int ssh_server_gss_kex_process_init(ssh_session session, ssh_buffer packet)
if (!(ret_flags & GSS_C_INTEG_FLAG) || !(ret_flags & GSS_C_MUTUAL_FLAG)) {
SSH_LOG(SSH_LOG_WARN,
"GSSAPI(accept) integrity and mutual flags were not set");
gss_release_buffer(&min_stat, &output_token);
goto error;
}
SSH_LOG(SSH_LOG_DEBUG, "token accepted");
@@ -607,6 +608,7 @@ int ssh_server_gss_kex_process_init(ssh_session session, ssh_buffer packet)
"creating mic failed",
maj_stat,
min_stat);
gss_release_buffer(&min_stat, &output_token);
goto error;
}
@@ -621,15 +623,14 @@ int ssh_server_gss_kex_process_init(ssh_session session, ssh_buffer packet)
output_token.length,
(size_t)output_token.length,
output_token.value);
gss_release_buffer(&min_stat, &output_token);
gss_release_buffer(&min_stat, &mic);
if (rc != SSH_OK) {
ssh_set_error_oom(session);
ssh_buffer_reinit(session->out_buffer);
goto error;
}
gss_release_buffer(&min_stat, &output_token);
gss_release_buffer(&min_stat, &mic);
rc = ssh_packet_send(session);
if (rc == SSH_ERROR) {
goto error;

View File

@@ -507,3 +507,9 @@ LIBSSH_4_11_0 # Released
sshsig_verify;
} LIBSSH_4_10_0;
LIBSSH_AFTER_4_11_0
{
global:
ssh_bind_config_parse_string;
} LIBSSH_4_11_0;

View File

@@ -2454,7 +2454,7 @@ FILE *ssh_strict_fopen(const char *filename, size_t max_file_size)
/* open first to avoid TOCTOU */
fd = open(filename, O_RDONLY);
if (fd == -1) {
SSH_LOG(SSH_LOG_RARE,
SSH_LOG(SSH_LOG_TRACE,
"Failed to open a file %s for reading: %s",
filename,
ssh_strerror(errno, err_msg, SSH_ERRNO_MSG_MAX));
@@ -2464,7 +2464,7 @@ FILE *ssh_strict_fopen(const char *filename, size_t max_file_size)
/* Check the file is sensible for a configuration file */
r = fstat(fd, &sb);
if (r != 0) {
SSH_LOG(SSH_LOG_RARE,
SSH_LOG(SSH_LOG_TRACE,
"Failed to stat %s: %s",
filename,
ssh_strerror(errno, err_msg, SSH_ERRNO_MSG_MAX));
@@ -2472,7 +2472,7 @@ FILE *ssh_strict_fopen(const char *filename, size_t max_file_size)
return NULL;
}
if ((sb.st_mode & S_IFMT) != S_IFREG) {
SSH_LOG(SSH_LOG_RARE,
SSH_LOG(SSH_LOG_TRACE,
"The file %s is not a regular file: skipping",
filename);
close(fd);
@@ -2480,7 +2480,7 @@ FILE *ssh_strict_fopen(const char *filename, size_t max_file_size)
}
if ((size_t)sb.st_size > max_file_size) {
SSH_LOG(SSH_LOG_RARE,
SSH_LOG(SSH_LOG_TRACE,
"The file %s is too large (%jd MB > %zu MB): skipping",
filename,
(intmax_t)sb.st_size / 1024 / 1024,
@@ -2491,7 +2491,7 @@ FILE *ssh_strict_fopen(const char *filename, size_t max_file_size)
f = fdopen(fd, "r");
if (f == NULL) {
SSH_LOG(SSH_LOG_RARE,
SSH_LOG(SSH_LOG_TRACE,
"Failed to open a file %s for reading: %s",
filename,
ssh_strerror(r, err_msg, SSH_ERRNO_MSG_MAX));

View File

@@ -2401,6 +2401,15 @@ static int ssh_bind_set_algo(ssh_bind sshbind,
* not a pointer when it should have been a pointer, or if
* its a pointer to a pointer when it should have just been
* a pointer), then the behaviour is undefined.
*
* @warning Options set via this function may be overridden if a
* configuration file is parsed afterwards (e.g., by an
* implicit call to ssh_bind_options_parse_config() inside
* ssh_bind_listen(), or by a manual call to the same
* function) and contains the same options.\n
* It is the callers responsibility to ensure the correct
* order of API calls if explicit options must take
* precedence.
*/
int
ssh_bind_options_set(ssh_bind sshbind,

View File

@@ -1343,6 +1343,15 @@ process_opendir(sftp_client_message client_msg)
}
h->dirp = dir;
h->name = strdup(dir_name);
if (h->name == NULL) {
free(h);
closedir(dir);
SSH_LOG(SSH_LOG_PROTOCOL, "failed to duplicate directory name");
sftp_reply_status(client_msg,
SSH_FX_FAILURE,
"Failed to allocate new handle");
return SSH_ERROR;
}
h->type = SFTP_DIR_HANDLE;
handle_s = sftp_handle_alloc(client_msg->sftp, h);
@@ -1350,6 +1359,7 @@ process_opendir(sftp_client_message client_msg)
sftp_reply_handle(client_msg, handle_s);
ssh_string_free(handle_s);
} else {
SAFE_FREE(h->name);
free(h);
closedir(dir);
sftp_reply_status(client_msg, SSH_FX_FAILURE, "No handle available");

View File

@@ -1435,7 +1435,7 @@ ssh_socket_connect_proxyjump(ssh_socket s)
session = s->session;
SSH_LOG(SSH_LOG_INFO,
SSH_LOG(SSH_LOG_DEBUG,
"Connecting to host %s port %d user %s through ProxyJump",
session->opts.host,
session->opts.port,
@@ -1515,7 +1515,7 @@ ssh_socket_connect_proxyjump(ssh_socket s)
/* transferred to the jump_thread_data */
jump_session = NULL;
SSH_LOG(SSH_LOG_INFO,
SSH_LOG(SSH_LOG_DEBUG,
"Starting proxy thread to host %s port %d user %s, callbacks=%p",
jump_thread_data->next_jump->hostname,
jump_thread_data->next_jump->port,

View File

@@ -259,6 +259,20 @@ else()
set(PUTTYGEN_EXECUTABLE "/bin/puttygen-not-found")
endif()
find_program(TINYSSHD_EXECUTABLE
NAMES
tinysshd
PATHS
/sbin
/usr/sbin
/usr/local/sbin)
if (TINYSSHD_EXECUTABLE)
message(STATUS "Found TinySSH server: ${TINYSSHD_EXECUTABLE}")
else()
message(STATUS "Could NOT find TinySSH server")
endif()
find_program(SSHD_EXECUTABLE
NAME
sshd
@@ -391,6 +405,14 @@ if (CLIENT_TESTING OR SERVER_TESTING)
file(COPY keys/id_ed25519_sk DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/home/bob/.ssh/ FILE_PERMISSIONS OWNER_READ OWNER_WRITE)
file(COPY keys/id_ed25519_sk.pub DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/home/bob/.ssh/ FILE_PERMISSIONS OWNER_READ OWNER_WRITE)
# TINYSSH Key Setup
set(TINYSSH_KEY_DIR "${CMAKE_CURRENT_BINARY_DIR}/etc/tinyssh")
file(MAKE_DIRECTORY ${TINYSSH_KEY_DIR})
# Copy the TINYSSH hostkeys
file(COPY keys/ed25519.pk DESTINATION ${TINYSSH_KEY_DIR})
file(COPY keys/.ed25519.sk DESTINATION ${TINYSSH_KEY_DIR})
# Allow to auth with bob's public keys on alice and doe account
configure_file(keys/id_rsa.pub ${CMAKE_CURRENT_BINARY_DIR}/home/alice/.ssh/authorized_keys @ONLY)
configure_file(keys/id_rsa.pub ${CMAKE_CURRENT_BINARY_DIR}/home/doe/.ssh/authorized_keys @ONLY)
@@ -419,6 +441,9 @@ if (CLIENT_TESTING OR SERVER_TESTING)
file(READ keys/pkcs11/id_pkcs11_ecdsa_256_openssh.pub CONTENTS)
file(APPEND ${CMAKE_CURRENT_BINARY_DIR}/home/charlie/.ssh/authorized_keys "${CONTENTS}")
# Create home directory for noneuser (for "none" authentication test)
file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/home/noneuser/.ssh)
file(READ keys/pkcs11/id_pkcs11_ecdsa_384_openssh.pub CONTENTS)
file(APPEND ${CMAKE_CURRENT_BINARY_DIR}/home/charlie/.ssh/authorized_keys "${CONTENTS}")

View File

@@ -36,6 +36,10 @@ if (WITH_PKCS11_URI)
torture_auth_pkcs11)
endif()
if (TINYSSHD_EXECUTABLE AND NCAT_EXECUTABLE)
set(LIBSSH_CLIENT_TESTS ${LIBSSH_CLIENT_TESTS} torture_tinyssh)
endif()
if (HAVE_PTHREAD)
set(LIBSSH_CLIENT_TESTS
${LIBSSH_CLIENT_TESTS}
@@ -92,6 +96,12 @@ foreach(_CLI_TEST ${LIBSSH_CLIENT_TESTS})
LINK_LIBRARIES ${TORTURE_LIBRARY} util
)
if (_CLI_TEST STREQUAL "torture_tinyssh")
target_compile_definitions(${_CLI_TEST} PRIVATE
TINYSSH_KEYS_DIR="${TINYSSH_KEY_DIR}"
)
endif()
if (OSX)
set_property(
TEST

View File

@@ -228,6 +228,44 @@ static void torture_auth_none_max_tries(void **state) {
torture_update_sshd_config(state, "");
}
static void torture_auth_none_success(void **state)
{
struct torture_state *s = *state;
const char *additional_config = "PermitEmptyPasswords yes\n"
"PasswordAuthentication yes\n"
"KbdInteractiveAuthentication no\n"
"PubkeyAuthentication no\n"
"AuthenticationMethods none\n";
ssh_session session = s->ssh.session;
int rc;
torture_update_sshd_config(state, additional_config);
/* Use noneuser which has an empty password set in shadow.in
* When PermitEmptyPasswords is yes and PasswordAuthentication is yes,
* OpenSSH's userauth_none() internally calls mm_auth_password() with
* an empty password, which succeeds for users with empty passwords.
*/
rc = ssh_options_set(session, SSH_OPTIONS_USER, TORTURE_SSH_USER_NONEUSER);
if (rc != SSH_OK) {
goto cleanup;
}
rc = ssh_connect(session);
if (rc != SSH_OK) {
goto cleanup;
}
rc = ssh_userauth_none(session, NULL);
assert_int_equal(rc, SSH_AUTH_SUCCESS);
cleanup:
torture_update_sshd_config(state, "");
if (rc != SSH_OK && rc != SSH_AUTH_SUCCESS) {
assert_int_equal(rc, SSH_OK);
}
}
static void torture_auth_pubkey(void **state) {
struct torture_state *s = *state;
@@ -1373,6 +1411,9 @@ int torture_run_tests(void) {
cmocka_unit_test_setup_teardown(torture_auth_none_nonblocking,
session_setup,
session_teardown),
cmocka_unit_test_setup_teardown(torture_auth_none_success,
session_setup,
session_teardown),
cmocka_unit_test_setup_teardown(torture_auth_none_max_tries,
session_setup,
session_teardown),
@@ -1424,9 +1465,10 @@ int torture_run_tests(void) {
cmocka_unit_test_setup_teardown(torture_auth_agent_identities_only,
agent_setup,
agent_teardown),
cmocka_unit_test_setup_teardown(torture_auth_agent_identities_only_protected,
agent_setup,
agent_teardown),
cmocka_unit_test_setup_teardown(
torture_auth_agent_identities_only_protected,
agent_setup,
agent_teardown),
cmocka_unit_test_setup_teardown(torture_auth_pubkey_types,
pubkey_setup,
session_teardown),
@@ -1436,15 +1478,17 @@ int torture_run_tests(void) {
cmocka_unit_test_setup_teardown(torture_auth_pubkey_types_ecdsa,
pubkey_setup,
session_teardown),
cmocka_unit_test_setup_teardown(torture_auth_pubkey_types_ecdsa_nonblocking,
pubkey_setup,
session_teardown),
cmocka_unit_test_setup_teardown(
torture_auth_pubkey_types_ecdsa_nonblocking,
pubkey_setup,
session_teardown),
cmocka_unit_test_setup_teardown(torture_auth_pubkey_types_ed25519,
pubkey_setup,
session_teardown),
cmocka_unit_test_setup_teardown(torture_auth_pubkey_types_ed25519_nonblocking,
pubkey_setup,
session_teardown),
cmocka_unit_test_setup_teardown(
torture_auth_pubkey_types_ed25519_nonblocking,
pubkey_setup,
session_teardown),
#ifdef WITH_FIDO2
cmocka_unit_test_setup_teardown(torture_auth_pubkey_types_sk_ecdsa,
pubkey_setup,
@@ -1456,9 +1500,10 @@ int torture_run_tests(void) {
cmocka_unit_test_setup_teardown(torture_auth_pubkey_rsa_key_size,
pubkey_setup,
session_teardown),
cmocka_unit_test_setup_teardown(torture_auth_pubkey_rsa_key_size_nonblocking,
pubkey_setup,
session_teardown),
cmocka_unit_test_setup_teardown(
torture_auth_pubkey_rsa_key_size_nonblocking,
pubkey_setup,
session_teardown),
cmocka_unit_test_setup_teardown(torture_auth_pubkey_skip_none,
pubkey_setup,
session_teardown),

View File

@@ -0,0 +1,328 @@
/*
* This file is part of the SSH Library
*
* Copyright (c) 2026 by Your Bulitha Kawushika De Zoysa
*
* The SSH Library is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or (at your
* option) any later version.
*
* The SSH Library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
* License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with the SSH Library; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston,
* MA 02111-1307, USA.
*/
#include "config.h"
#include "tests_config.h"
#define LIBSSH_STATIC
#include "libssh/libssh.h"
#include "libssh/priv.h"
#include "libssh/session.h"
#include "torture.h"
#include <errno.h>
#include <pwd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define TINYSSH_PIDFILE "tinyssh.pid"
#define TINYSSH_PORT 22
/* TINYSSH Server Setup and Teardown */
static int tinyssh_setup(void **state)
{
struct torture_state *s = NULL;
char cmd[4096];
char pid_path[1024];
int rc;
torture_setup_socket_dir(state);
s = *state;
snprintf(pid_path,
sizeof(pid_path),
"%s/%s",
s->socket_dir,
TINYSSH_PIDFILE);
free(s->srv_pidfile);
s->srv_pidfile = strdup(pid_path);
if (s->srv_pidfile == NULL) {
return -1;
}
snprintf(cmd,
sizeof(cmd),
"%s -l %s %d -k -c \"%s %s -v %s\" "
"> %s/tinyssh.log 2>&1 & echo $! > %s",
NCAT_EXECUTABLE,
TORTURE_SSH_SERVER,
TINYSSH_PORT,
TINYSSHD_EXECUTABLE,
"",
TINYSSH_KEYS_DIR,
s->socket_dir,
s->srv_pidfile);
SSH_LOG(SSH_LOG_DEBUG, "Executing: %s\n", cmd);
rc = system(cmd);
if (rc != 0) {
return -1;
}
rc = torture_wait_for_daemon(15);
if (rc != 0) {
return -1;
}
return 0;
}
static int tinyssh_teardown(void **state)
{
struct torture_state *s = *state;
torture_terminate_process(s->srv_pidfile);
torture_teardown_socket_dir(state);
return 0;
}
/* LIBSSH Client Setup and Teardown */
static int session_setup(void **state)
{
struct torture_state *s = *state;
int verbosity = torture_libssh_verbosity();
bool process_config = false;
int port = TINYSSH_PORT;
struct passwd *pwd = NULL;
int rc;
pwd = getpwnam("bob");
assert_non_null(pwd);
rc = setuid(pwd->pw_uid);
assert_return_code(rc, errno);
s->ssh.session = ssh_new();
assert_non_null(s->ssh.session);
ssh_options_set(s->ssh.session, SSH_OPTIONS_LOG_VERBOSITY, &verbosity);
ssh_options_set(s->ssh.session, SSH_OPTIONS_HOST, TORTURE_SSH_SERVER);
ssh_options_set(s->ssh.session, SSH_OPTIONS_PORT, &port);
ssh_options_set(s->ssh.session, SSH_OPTIONS_USER, "bob");
ssh_options_set(s->ssh.session,
SSH_OPTIONS_PROCESS_CONFIG,
&process_config);
return 0;
}
static int session_teardown(void **state)
{
struct torture_state *s = *state;
if (s->ssh.session) {
ssh_disconnect(s->ssh.session);
ssh_free(s->ssh.session);
}
return 0;
}
/* Algorithms Helper */
static void test_specific_algorithm(ssh_session session,
const char *kex,
const char *cipher,
const char *hostkey,
int expected_rc)
{
int rc;
char data[256];
size_t len_to_test[] = {1, 2, 3, 4, 5, 6, 7, 8, 10, 12, 15,
16, 20, 31, 32, 33, 63, 64, 65, 100, 127, 128};
unsigned int i;
/* Set Key Exchange */
if (kex != NULL) {
rc = ssh_options_set(session, SSH_OPTIONS_KEY_EXCHANGE, kex);
assert_ssh_return_code(session, rc);
}
/* Set Ciphers */
if (cipher != NULL) {
rc = ssh_options_set(session, SSH_OPTIONS_CIPHERS_C_S, cipher);
assert_ssh_return_code(session, rc);
rc = ssh_options_set(session, SSH_OPTIONS_CIPHERS_S_C, cipher);
assert_ssh_return_code(session, rc);
}
/* Set Hostkey */
if (hostkey != NULL) {
rc = ssh_options_set(session, SSH_OPTIONS_HOSTKEYS, hostkey);
assert_ssh_return_code(session, rc);
}
rc = ssh_connect(session);
if (expected_rc == SSH_OK) {
assert_ssh_return_code(session, rc);
if (cipher != NULL) {
const char *used_cipher = ssh_get_cipher_out(session);
assert_non_null(used_cipher);
assert_string_equal(used_cipher, cipher);
}
if (hostkey != NULL) {
ssh_key pubkey = NULL;
const char *type_str = NULL;
rc = ssh_get_server_publickey(session, &pubkey);
assert_int_equal(rc, SSH_OK);
assert_non_null(pubkey);
type_str = ssh_key_type_to_char(ssh_key_type(pubkey));
assert_non_null(type_str);
assert_string_equal(type_str, hostkey);
ssh_key_free(pubkey);
}
memset(data, 0, sizeof(data));
for (i = 0; i < (sizeof(len_to_test) / sizeof(size_t)); i++) {
memset(data, 'A', len_to_test[i]);
ssh_send_ignore(session, data);
ssh_handle_packets(session, 50);
}
rc = ssh_userauth_none(session, NULL);
if (rc != SSH_OK) {
rc = ssh_get_error_code(session);
assert_int_equal(rc, SSH_REQUEST_DENIED);
}
} else {
assert_int_not_equal(rc, SSH_OK);
}
}
/* Test Cases */
static void torture_tinyssh_curve25519(void **state)
{
struct torture_state *s = *state;
test_specific_algorithm(s->ssh.session,
"curve25519-sha256",
NULL,
NULL,
SSH_OK);
}
static void torture_tinyssh_curve25519_libssh(void **state)
{
struct torture_state *s = *state;
test_specific_algorithm(s->ssh.session,
"curve25519-sha256@libssh.org",
NULL,
NULL,
SSH_OK);
}
static void torture_tinyssh_sntrup761(void **state)
{
struct torture_state *s = *state;
test_specific_algorithm(s->ssh.session,
"sntrup761x25519-sha512@openssh.com",
NULL,
NULL,
SSH_OK);
}
static void torture_tinyssh_chacha20(void **state)
{
struct torture_state *s = *state;
test_specific_algorithm(s->ssh.session,
NULL,
"chacha20-poly1305@openssh.com",
NULL,
SSH_OK);
}
static void torture_tinyssh_neg_cipher(void **state)
{
struct torture_state *s = *state;
/* TinySSH does not support older ciphers like aes128-cbc.*/
test_specific_algorithm(s->ssh.session,
NULL,
"aes128-cbc",
NULL,
SSH_ERROR);
}
static void torture_tinyssh_hostkey_ed25519(void **state)
{
struct torture_state *s = *state;
test_specific_algorithm(s->ssh.session, NULL, NULL, "ssh-ed25519", SSH_OK);
}
static void torture_tinyssh_neg_kex(void **state)
{
struct torture_state *s = *state;
/* TinySSH does not support legacy Diffie-Hellman groups or NIST curves.*/
test_specific_algorithm(s->ssh.session,
"diffie-hellman-group1-sha1",
NULL,
NULL,
SSH_ERROR);
}
int torture_run_tests(void)
{
int rc;
struct CMUnitTest tests[] = {
cmocka_unit_test_setup_teardown(torture_tinyssh_curve25519,
session_setup,
session_teardown),
cmocka_unit_test_setup_teardown(torture_tinyssh_curve25519_libssh,
session_setup,
session_teardown),
cmocka_unit_test_setup_teardown(torture_tinyssh_sntrup761,
session_setup,
session_teardown),
cmocka_unit_test_setup_teardown(torture_tinyssh_hostkey_ed25519,
session_setup,
session_teardown),
cmocka_unit_test_setup_teardown(torture_tinyssh_chacha20,
session_setup,
session_teardown),
cmocka_unit_test_setup_teardown(torture_tinyssh_neg_cipher,
session_setup,
session_teardown),
cmocka_unit_test_setup_teardown(torture_tinyssh_neg_kex,
session_setup,
session_teardown),
};
ssh_init();
torture_filter_tests(tests);
rc = cmocka_run_group_tests(tests, tinyssh_setup, tinyssh_teardown);
ssh_finalize();
return rc;
}

View File

@@ -2,3 +2,4 @@ bob:secret:sshd
alice:secret:sshd
charlie:secret:sshd
doe:secret:sshd
noneuser::sshd

View File

@@ -3,6 +3,7 @@ alice:x:5001:9000:alice gecos:@HOMEDIR@/alice:/bin/sh
charlie:x:5002:9000:charlie gecos:@HOMEDIR@/charlie:/bin/sh
doe:x:5003:9000:doe gecos:@HOMEDIR@/doe:/bin/sh
frank:x:5003:9000:doe gecos:@HOMEDIR@/frank:/bin/sh
noneuser:x:5004:9000:noneuser gecos:@HOMEDIR@/noneuser:/bin/sh
sshd:x:65530:65531:sshd:@HOMEDIR@:/sbin/nologin
nobody:x:65533:65534:nobody gecos:@HOMEDIR@:/bin/false
root:x:0:0:root gecos:@HOMEDIR@:/bin/false

View File

@@ -2,3 +2,4 @@ alice:$6$0jWkA8VP$MvBUvtGy38jWCZ5KtqnZEKQWXvvImDkDhDQII1kTqtAp3/xH31b71c.AjGkBFl
bob:$6$0jWkA8VP$MvBUvtGy38jWCZ5KtqnZEKQWXvvImDkDhDQII1kTqtAp3/xH31b71c.AjGkBFle.2QwCJQH7OzB/NXiMprusr/::0:::::
charlie:$6$0jWkA8VP$MvBUvtGy38jWCZ5KtqnZEKQWXvvImDkDhDQII1kTqtAp3/xH31b71c.AjGkBFle.2QwCJQH7OzB/NXiMprusr/::0:::::
doe:$6$0jWkA8VP$MvBUvtGy38jWCZ5KtqnZEKQWXvvImDkDhDQII1kTqtAp3/xH31b71c.AjGkBFle.2QwCJQH7OzB/NXiMprusr/::0:::::
noneuser:::0:::::

View File

@@ -33,6 +33,7 @@ fuzzer(ssh_client_config_fuzzer)
fuzzer(ssh_known_hosts_fuzzer)
fuzzer(ssh_privkey_fuzzer)
fuzzer(ssh_pubkey_fuzzer)
fuzzer(ssh_sftp_attr_fuzzer)
fuzzer(ssh_sshsig_fuzzer)
if (WITH_SERVER)
fuzzer(ssh_server_fuzzer)

View File

@@ -0,0 +1,131 @@
/*
* 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 <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define LIBSSH_STATIC 1
#include "libssh/libssh.h"
#include "libssh/sftp.h"
#include "libssh/sftp_priv.h"
#include "nallocinc.c"
/* SFTP protocol version constants */
#define SFTP_PROTOCOL_VERSION_3 3
#define SFTP_PROTOCOL_VERSION_4 4
/* Flags for sftp_parse_attr expectname parameter */
#define SFTP_EXPECT_NAME 1
#define SFTP_NO_NAME 0
/*
* Helper to create a minimal sftp_session for fuzzing.
* We don't use sftp_new() as it requires a real SSH connection.
*/
static sftp_session create_minimal_sftp_session(ssh_session session)
{
sftp_session sftp;
sftp = calloc(1, sizeof(struct sftp_session_struct));
if (sftp == NULL) {
return NULL;
}
sftp->session = session;
return sftp;
}
static void _fuzz_finalize(void)
{
ssh_finalize();
}
int LLVMFuzzerInitialize(int *argc, char ***argv)
{
(void)argc;
nalloc_init(*argv[0]);
ssh_init();
atexit(_fuzz_finalize);
return 0;
}
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
{
ssh_session session = NULL;
sftp_session sftp = NULL;
ssh_buffer buffer = NULL;
sftp_attributes attr = NULL;
int versions[] = {
SFTP_PROTOCOL_VERSION_3, SFTP_PROTOCOL_VERSION_3,
SFTP_PROTOCOL_VERSION_4, SFTP_PROTOCOL_VERSION_4
};
int expectnames[] = {SFTP_NO_NAME, SFTP_EXPECT_NAME, SFTP_NO_NAME, SFTP_EXPECT_NAME};
size_t i;
/* Minimum bytes for a valid SFTP message */
if (size == 0) {
return 0;
}
assert(nalloc_start(data, size) > 0);
/* Allocate shared resources once for all test iterations */
session = ssh_new();
if (session == NULL) {
goto cleanup;
}
sftp = create_minimal_sftp_session(session);
if (sftp == NULL) {
goto cleanup;
}
buffer = ssh_buffer_new();
if (buffer == NULL) {
goto cleanup;
}
/* Main fuzzing target: sftp_parse_attr */
/* Parses untrusted SFTP messages from client */
/* Test all combinations (v3/v4, with/without name) */
for (i = 0; i < (sizeof(versions) / sizeof(versions[0])); i++) {
sftp->version = versions[i];
/* Reset and repopulate buffer for each iteration */
ssh_buffer_reinit(buffer);
if (ssh_buffer_add_data(buffer, data, size) == SSH_OK) {
attr = sftp_parse_attr(sftp, buffer, expectnames[i]);
sftp_attributes_free(attr);
attr = NULL;
}
}
cleanup:
ssh_buffer_free(buffer);
free(sftp);
ssh_free(session);
nalloc_end();
return 0;
}

BIN
tests/keys/.ed25519.sk Normal file

Binary file not shown.

1
tests/keys/ed25519.pk Normal file
View File

@@ -0,0 +1 @@
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>C<EFBFBD><EFBFBD>K<EFBFBD>ݛ<EFBFBD>1<1C><>'j &<26>e<EFBFBD><65><EFBFBD><EF8A8D>

View File

@@ -1130,6 +1130,46 @@ static void torture_server_sftp_handles_exhaustion(void **state)
}
}
static void torture_server_sftp_opendir_handles_exhaustion(void **state)
{
struct test_server_st *tss = *state;
struct torture_state *s = NULL;
struct torture_sftp *tsftp = NULL;
char name[128] = {0};
sftp_file handles[SFTP_HANDLES] = {0};
sftp_dir dir = NULL;
sftp_session sftp = NULL;
int rc;
assert_non_null(tss);
s = tss->state;
assert_non_null(s);
tsftp = s->ssh.tsftp;
assert_non_null(tsftp);
sftp = tsftp->sftp;
assert_non_null(sftp);
/* Occupy all handles with files */
for (int i = 0; i < SFTP_HANDLES; i++) {
snprintf(name, sizeof(name), "%s/fn%d", tsftp->testdir, i);
handles[i] = sftp_open(sftp, name, O_WRONLY | O_CREAT, 0700);
assert_non_null(handles[i]);
}
/* Opening a directory should fail gracefully without leaking h->name */
dir = sftp_opendir(sftp, tsftp->testdir);
assert_null(dir);
/* cleanup */
for (int i = 0; i < SFTP_HANDLES; i++) {
rc = sftp_close(handles[i]);
assert_int_equal(rc, SSH_OK);
}
}
static void torture_server_sftp_handle_overrun(void **state)
{
struct test_server_st *tss = *state;
@@ -1290,6 +1330,9 @@ int torture_run_tests(void) {
cmocka_unit_test_setup_teardown(torture_server_sftp_handles_exhaustion,
session_setup_sftp,
session_teardown),
cmocka_unit_test_setup_teardown(torture_server_sftp_opendir_handles_exhaustion,
session_setup_sftp,
session_teardown),
cmocka_unit_test_setup_teardown(torture_server_sftp_handle_overrun,
session_setup_sftp,
session_teardown),

View File

@@ -86,3 +86,7 @@
#cmakedefine PKCS11SPY "${PKCS11SPY}"
#cmakedefine HAVE_SK_DUMMY 1
#cmakedefine SK_DUMMY_LIBRARY_PATH "${SK_DUMMY_LIBRARY_PATH}"
/* TinySSH Executable */
#cmakedefine TINYSSHD_EXECUTABLE "${TINYSSHD_EXECUTABLE}"

View File

@@ -51,6 +51,7 @@
#define TORTURE_SSH_USER_ALICE "alice"
#define TORTURE_SSH_USER_CHARLIE "charlie"
#define TORTURE_SSH_USER_NONEUSER "noneuser"
/* Used by main to communicate with parse_opt. */
struct argument_s {