- file: open -K dirlink referents via a race-safe relative O_NOFOLLOW walk from the authorized-root fd instead of re-opening an absolute realpath() result (removes the intermediate-symlink swap TOCTOU). - transport_ssh: always single-quote the server path, including --old-args, so no mode can inject shell metacharacters. - transport_tls: set SSL_OP_NO_COMPRESSION and (guarded) SSL_OP_NO_RENEGOTIATION. - credentials: reject --password-file/--early-input with any group/other permission bit; chmod 0600 the affected test fixtures. - file_store: export file_store_write_sparse() and remove the verbatim file.c duplicate.
171 lines
6.3 KiB
C
171 lines
6.3 KiB
C
#include "test_transport_ssh.h"
|
|
#include "test_utils.h"
|
|
#include "transport_ssh.h"
|
|
|
|
static void test_ssh_connect_invalid_dest_no_colon() {
|
|
/* cppcheck-suppress constVariablePointer */
|
|
Client* client =
|
|
client_connect_ssh("invalid-destination-no-colon", 22, NULL, false, NULL, false, NULL, 0);
|
|
EXPECT_NULL(client);
|
|
}
|
|
|
|
static void test_ssh_connect_invalid_dest_empty() {
|
|
/* cppcheck-suppress constVariablePointer */
|
|
Client* client = client_connect_ssh("", 22, NULL, false, NULL, false, NULL, 0);
|
|
EXPECT_NULL(client);
|
|
}
|
|
|
|
/* A child that cannot exec ssh must not be returned as a successful client. */
|
|
static void test_ssh_connect_malformed() {
|
|
const char* old_path = getenv("PATH");
|
|
char* saved_path = old_path ? strdup(old_path) : NULL;
|
|
setenv("PATH", "", 1);
|
|
|
|
/* cppcheck-suppress constVariablePointer */
|
|
Client* client = client_connect_ssh(":", 22, NULL, false, NULL, false, NULL, 0);
|
|
|
|
if (saved_path) {
|
|
setenv("PATH", saved_path, 1);
|
|
free(saved_path);
|
|
} else {
|
|
unsetenv("PATH");
|
|
}
|
|
|
|
EXPECT_NULL(client);
|
|
}
|
|
|
|
/* Test client_connect_ssh with valid format but unreachable host.
|
|
* The function launches ssh which will fail to connect, returns a Client. */
|
|
static void test_ssh_connect_unreachable() {
|
|
Client* client =
|
|
client_connect_ssh("nonexistent.invalid:/remote/path", 22, NULL, false, NULL, false, NULL, 0);
|
|
if (client != NULL) {
|
|
client_disconnect(client);
|
|
client_delete(client);
|
|
}
|
|
EXPECT_TRUE(true);
|
|
}
|
|
|
|
static void test_ssh_remote_command_argument_modes() {
|
|
char* command = ssh_build_remote_command("fast sync; touch /tmp/pwned", false, NULL, 0);
|
|
EXPECT_EQ_STR(command, "'fast sync; touch /tmp/pwned' --stdio");
|
|
free(command);
|
|
|
|
command = ssh_build_remote_command("fast'sync", false, NULL, 0);
|
|
EXPECT_EQ_STR(command, "'fast'\\''sync' --stdio");
|
|
free(command);
|
|
|
|
/* --old-args no longer disables injection-safe quoting: the path is still one
|
|
single-quoted word, even when it carries shell metacharacters. */
|
|
command = ssh_build_remote_command("fast sync; touch /tmp/pwned", true, NULL, 0);
|
|
EXPECT_EQ_STR(command, "'fast sync; touch /tmp/pwned' --stdio");
|
|
free(command);
|
|
|
|
command = ssh_build_remote_command("fast'sync; rm -rf /", true, NULL, 0);
|
|
EXPECT_EQ_STR(command, "'fast'\\''sync; rm -rf /' --stdio");
|
|
free(command);
|
|
}
|
|
|
|
/* The build for a single-word argv is [prog, six -o args, user, command]. */
|
|
|
|
static void test_ssh_build_client_argv_default_is_ssh() {
|
|
char** argv = ssh_build_client_argv(NULL, 0, "u@h", "'srv' --stdio");
|
|
EXPECT_NOT_NULL(argv);
|
|
EXPECT_EQ_STR(argv[0], "ssh");
|
|
EXPECT_EQ_STR(argv[1], "-o");
|
|
EXPECT_EQ_STR(argv[7], "u@h");
|
|
EXPECT_EQ_STR(argv[8], "'srv' --stdio");
|
|
EXPECT_NULL(argv[9]);
|
|
ssh_free_client_argv(argv);
|
|
}
|
|
|
|
/* A configured rsh must replace "ssh" as argv[0] (and never leak the default). */
|
|
static void test_ssh_build_client_argv_uses_custom_rsh() {
|
|
char** argv = ssh_build_client_argv("myrsh", 0, "u@h", "rc");
|
|
EXPECT_NOT_NULL(argv);
|
|
EXPECT_EQ_STR(argv[0], "myrsh");
|
|
EXPECT_NULL(argv[9]);
|
|
ssh_free_client_argv(argv);
|
|
}
|
|
|
|
/* A multi-word rsh command line (rsync -e "ssh -p 2222") is split into the
|
|
* leading argv words; a non-default port adds a -p/value pair. */
|
|
static void test_ssh_build_client_argv_whitespace_command_and_port() {
|
|
char** argv = ssh_build_client_argv("ssh -p 2222", 0, "u@h", "rc");
|
|
EXPECT_NOT_NULL(argv);
|
|
EXPECT_EQ_STR(argv[0], "ssh");
|
|
EXPECT_EQ_STR(argv[1], "-p");
|
|
EXPECT_EQ_STR(argv[2], "2222");
|
|
EXPECT_NULL(argv[11]);
|
|
ssh_free_client_argv(argv);
|
|
|
|
argv = ssh_build_client_argv("ssh", 2222, "u@h", "rc");
|
|
EXPECT_NOT_NULL(argv);
|
|
EXPECT_EQ_STR(argv[0], "ssh");
|
|
/* Flat [prog, -o x6, -p, port, user, command]. */
|
|
EXPECT_EQ_STR(argv[7], "-p");
|
|
EXPECT_EQ_STR(argv[8], "2222");
|
|
EXPECT_EQ_STR(argv[9], "u@h");
|
|
EXPECT_EQ_STR(argv[10], "rc");
|
|
EXPECT_NULL(argv[11]);
|
|
ssh_free_client_argv(argv);
|
|
}
|
|
|
|
/* --remote-option=OPT appends OPT to the remote command line after " --stdio",
|
|
* each escaped as its own single-quoted shell word. Metacharacters that could
|
|
* break out of the quoting are neutralized (never injected), matching the
|
|
* ssh_build_remote_command safety boundary for the server path. */
|
|
static void test_ssh_remote_command_with_remote_options() {
|
|
char* noop[] = {"--allow-delete"};
|
|
char* command = ssh_build_remote_command("fastsync-server", false, noop, 1);
|
|
EXPECT_EQ_STR(command, "'fastsync-server' --stdio '--allow-delete'");
|
|
free(command);
|
|
|
|
/* Multiple options append in order, each as its own quoted word. */
|
|
char* multi[] = {"-v", "--allow-delete"};
|
|
command = ssh_build_remote_command("srv", false, multi, 2);
|
|
EXPECT_EQ_STR(command, "'srv' --stdio '-v' '--allow-delete'");
|
|
free(command);
|
|
|
|
/* A remote option containing a single quote and shell metacharacters is
|
|
escaped with the same "'\''" boundary, so it stays one word and cannot
|
|
break out into an arbitrary remote command. */
|
|
char* val = strdup("--x=un'der; touch /tmp/pwned");
|
|
char* dangerous[1] = {val};
|
|
command = ssh_build_remote_command("srv", false, dangerous, 1);
|
|
EXPECT_EQ_STR(command, "'srv' --stdio '--x=un'\\''der; touch /tmp/pwned'");
|
|
free(command);
|
|
free(val);
|
|
|
|
/* --old-args still quotes both the server path and the remote options. */
|
|
command = ssh_build_remote_command("srv", true, multi, 2);
|
|
EXPECT_EQ_STR(command, "'srv' --stdio '-v' '--allow-delete'");
|
|
free(command);
|
|
}
|
|
|
|
/* The remote command builder refuses to forward an empty or control-character
|
|
* remote option (defense-in-depth independent of the CLI validation). */
|
|
static void test_ssh_remote_command_rejects_bad_options() {
|
|
char* empty[] = {""};
|
|
EXPECT_NULL(ssh_build_remote_command("srv", false, empty, 1));
|
|
|
|
char nl = '\n';
|
|
char* newline[] = {&nl};
|
|
EXPECT_NULL(ssh_build_remote_command("srv", false, newline, 1));
|
|
|
|
char* with_null[] = {NULL};
|
|
EXPECT_NULL(ssh_build_remote_command("srv", false, with_null, 1));
|
|
}
|
|
|
|
void test_transport_ssh() {
|
|
test_ssh_connect_invalid_dest_no_colon();
|
|
test_ssh_connect_invalid_dest_empty();
|
|
test_ssh_connect_malformed();
|
|
test_ssh_connect_unreachable();
|
|
test_ssh_remote_command_argument_modes();
|
|
test_ssh_build_client_argv_default_is_ssh();
|
|
test_ssh_build_client_argv_uses_custom_rsh();
|
|
test_ssh_build_client_argv_whitespace_command_and_port();
|
|
test_ssh_remote_command_with_remote_options();
|
|
test_ssh_remote_command_rejects_bad_options();
|
|
} |