diff --git a/cmdline/unix.c b/cmdline/unix.c index 4e9ef955..5cd604f6 100644 --- a/cmdline/unix.c +++ b/cmdline/unix.c @@ -21,6 +21,115 @@ int eaccess(const char* pathname, int mode) } #endif +/****************************************************************************/ +/* exec */ + +#if HAVE_LINUX_DEVICE +/** + * Validates a string for shell metacharacters. + * Returns -1 if dangerous characters are detected, 0 otherwise. + */ +static int validate_shell_input(const char* str) +{ + /* + * Define characters that can be used to inject commands or alter logic + * Includes: ; | & $ > < ` ' " ( ) [ * ? \ ~ and whitespace + */ + const char* danger_chars = ";|&$><`'\"()[]*?\\~\r\n "; + + /* check for any of the forbidden characters */ + if (strpbrk(str, danger_chars) != 0) + return -1; + + /* check if the string starts with a hyphen to prevent flag injection */ + if (str[0] == '-') + return -1; + + return 0; +} + +/** + * Validates a string for exec. + * Returns -1 if dangerous characters are detected, 0 otherwise. + */ +static int validate_exec_input(const char* str) +{ + /* reject paths trying to go up levels */ + if (strstr(str, "..") != 0) + return -1; + + /* reject inputs starting with '-' to prevent Flag Injection */ + if (str[0] == '-') + return -1; + + return 0; +} + + +/* + * Scrubbed environment + * Only provide the bare essentials. + */ +static char* const envp_scrubbed[] = { + "PATH=" +#ifdef __APPLE__ + "/opt/homebrew/bin:" +#endif + "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "TERM=dumb", + "LANG=C", + "IFS= \t\n", + NULL +}; + +/** + * Executes a command with arguments. + * @param args A NULL-terminated array of strings. args[0] is the executable. + * @return The exit status of the command, or -1 on fork/wait failure. + */ +static int systemv(const char* args[]) +{ + pid_t pid = fork(); + + if (pid < 0) { + /* LCOV_EXCL_START */ + return -1; + /* LCOV_EXCL_STOP */ + } + + if (pid == 0) { +#if defined(CLOSE_RANGE_CLOEXEC) && defined(HAVE_CLOSE_RANGE) + /* set all fd to be closed on exec as extra safety measure */ + close_range(3, ~0U, CLOSE_RANGE_CLOEXEC); +#endif + + /* child process */ + execve(args[0], (char* const*)args, envp_scrubbed); + _exit(EXIT_FAILURE); + } + + /* parent process */ + int status; + pid_t ret; + do { + ret = waitpid(pid, &status, 0); + } while (ret == -1 && errno == EINTR); + if (ret == -1) { + /* LCOV_EXCL_START */ + return -1; + /* LCOV_EXCL_STOP */ + } + + if (WIFEXITED(status)) { + return WEXITSTATUS(status); + } else if (WIFSIGNALED(status)) { + return -WTERMSIG(status); + } + + return -1; +} +#endif + /** * Exit codes. */ @@ -777,6 +886,12 @@ static int devdereference_zfs(uint64_t device, const char* dir, tommy_list* devl if (slash) *slash = 0; + if (validate_shell_input(pool) != 0) { + /* LCOV_EXCL_START */ + return -1; + /* LCOV_EXCL_STOP */ + } + snprintf(cmd, sizeof(cmd), "%s status -P %s 2>/dev/null", zpool, pool); FILE* fp = popen(cmd, "r"); @@ -2139,7 +2254,6 @@ int fssnapshot_btrfs_create(const struct fssnapshot_struct* fss, const char* nam static int fssnapshot_bcachefs_create(const struct fssnapshot_struct* fss, const char* name) { char target_path[PATH_MAX]; - char cmd[PATH_MAX * 3 + 64]; int ret; const char* bcachefs = find_bcachefs(); @@ -2152,9 +2266,23 @@ static int fssnapshot_bcachefs_create(const struct fssnapshot_struct* fss, const pathcpy(target_path, sizeof(target_path), fss->snapshot_dir); pathcat(target_path, sizeof(target_path), name); - snprintf(cmd, sizeof(cmd), "%s subvolume snapshot -r %s %s", bcachefs, fss->root_dir, target_path); + if (validate_exec_input(fss->root_dir) != 0 || validate_exec_input(target_path) != 0) { + /* LCOV_EXCL_START */ + return -1; + /* LCOV_EXCL_STOP */ + } - ret = system(cmd); + const char* argv[] = { + bcachefs, + "subvolume", + "snapshot", + "-r", + fss->root_dir, + target_path, + 0 + }; + + ret = systemv(argv); if (ret != 0) { /* LCOV_EXCL_START */ return -1; @@ -2168,7 +2296,7 @@ static int fssnapshot_bcachefs_create(const struct fssnapshot_struct* fss, const #if HAVE_LINUX_DEVICE static int fssnapshot_zfs_create(const struct fssnapshot_struct* fss, const char* name) { - char cmd[PATH_MAX * 2 + 64]; + char snapshot[PATH_MAX + 64]; int ret; const char* zfs = find_zfs(); @@ -2178,9 +2306,22 @@ static int fssnapshot_zfs_create(const struct fssnapshot_struct* fss, const char /* LCOV_EXCL_STOP */ } - snprintf(cmd, sizeof(cmd), "%s snapshot %s@%s", zfs, fss->dataset, name); + snprintf(snapshot, sizeof(snapshot), "%s@%s", fss->dataset, name); - ret = system(cmd); + if (validate_exec_input(snapshot) != 0) { + /* LCOV_EXCL_START */ + return -1; + /* LCOV_EXCL_STOP */ + } + + const char* argv[] = { + zfs, + "snapshot", + snapshot, + 0 + }; + + ret = systemv(argv); if (ret != 0) { /* LCOV_EXCL_START */ return -1; @@ -2244,7 +2385,6 @@ int fssnapshot_btrfs_delete(const struct fssnapshot_struct* fss, const char* nam static int fssnapshot_bcachefs_delete(const struct fssnapshot_struct* fss, const char* name) { char target_path[PATH_MAX]; - char cmd[PATH_MAX * 2 + 64]; int ret; const char* bcachefs = find_bcachefs(); @@ -2257,9 +2397,21 @@ static int fssnapshot_bcachefs_delete(const struct fssnapshot_struct* fss, const pathcpy(target_path, sizeof(target_path), fss->snapshot_dir); pathcat(target_path, sizeof(target_path), name); - snprintf(cmd, sizeof(cmd), "%s subvolume delete %s", bcachefs, target_path); + if (validate_exec_input(target_path) != 0) { + /* LCOV_EXCL_START */ + return -1; + /* LCOV_EXCL_STOP */ + } - ret = system(cmd); + const char* argv[] = { + bcachefs, + "subvolume", + "delete", + target_path, + 0, + }; + + ret = systemv(argv); if (ret != 0) { /* LCOV_EXCL_START */ return -1; @@ -2273,7 +2425,7 @@ static int fssnapshot_bcachefs_delete(const struct fssnapshot_struct* fss, const #if HAVE_LINUX_DEVICE static int fssnapshot_zfs_delete(const struct fssnapshot_struct* fss, const char* name) { - char cmd[PATH_MAX * 2 + 64]; + char snapshot[PATH_MAX + 64]; int ret; const char* zfs = find_zfs(); @@ -2283,9 +2435,22 @@ static int fssnapshot_zfs_delete(const struct fssnapshot_struct* fss, const char /* LCOV_EXCL_STOP */ } - snprintf(cmd, sizeof(cmd), "%s destroy %s@%s", zfs, fss->dataset, name); + snprintf(snapshot, sizeof(snapshot), "%s@%s", fss->dataset, name); - ret = system(cmd); + if (validate_exec_input(snapshot) != 0) { + /* LCOV_EXCL_START */ + return -1; + /* LCOV_EXCL_STOP */ + } + + const char* argv[] = { + zfs, + "destroy", + snapshot, + 0 + }; + + ret = systemv(argv); if (ret != 0) { /* LCOV_EXCL_START */ return -1; @@ -2358,7 +2523,8 @@ static int fssnapshot_bcachefs_rename(const struct fssnapshot_struct* fss, const #if HAVE_LINUX_DEVICE static int fssnapshot_zfs_rename(const struct fssnapshot_struct* fss, const char* old_name, const char* new_name) { - char cmd[PATH_MAX * 2 + 64]; + char old_snapshot[PATH_MAX + 64]; + char new_snapshot[PATH_MAX + 64]; int ret; const char* zfs = find_zfs(); @@ -2368,9 +2534,25 @@ static int fssnapshot_zfs_rename(const struct fssnapshot_struct* fss, const char /* LCOV_EXCL_STOP */ } - snprintf(cmd, sizeof(cmd), "%s rename %s@%s %s@%s", zfs, fss->dataset, old_name, fss->dataset, new_name); + snprintf(old_snapshot, sizeof(old_snapshot), "%s@%s", fss->dataset, old_name); + snprintf(new_snapshot, sizeof(new_snapshot), "%s@%s", fss->dataset, new_name); - ret = system(cmd); + if (validate_exec_input(old_snapshot) != 0 || validate_exec_input(new_snapshot) != 0) { + /* LCOV_EXCL_START */ + return -1; + /* LCOV_EXCL_STOP */ + } + + const char* argv[] = { + zfs, + "rename", + old_snapshot, + new_snapshot, + 0 + }; + + + ret = systemv(argv); if (ret != 0) { /* LCOV_EXCL_START */ return -1;