From 72bf79ba6b0458f24ed87250785d75881bef1987 Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Fri, 7 Feb 2025 12:38:04 +0100 Subject: [PATCH] Remove shell injection detection algo (#76) * Remove shell injection detection algo Let's add back when it's 100% * Fix assertion in wasm smoke test * Add dialect --- README.md | 11 +- smoketests/wasm.js | 2 +- src/ffi_bindings/mod.rs | 28 -- src/lib.rs | 1 - src/shell_injection/contains_shell_syntax.rs | 174 ------- .../contains_shell_syntax_test.rs | 171 ------- src/shell_injection/detect_shell_injection.rs | 31 -- .../detect_shell_injection_test.rs | 454 ------------------ src/shell_injection/is_safely_encapsulated.rs | 54 --- .../is_safely_encapsulated_test.rs | 55 --- src/shell_injection/mod.rs | 8 - src/wasm_bindings/mod.rs | 6 - 12 files changed, 8 insertions(+), 987 deletions(-) delete mode 100644 src/shell_injection/contains_shell_syntax.rs delete mode 100644 src/shell_injection/contains_shell_syntax_test.rs delete mode 100644 src/shell_injection/detect_shell_injection.rs delete mode 100644 src/shell_injection/detect_shell_injection_test.rs delete mode 100644 src/shell_injection/is_safely_encapsulated.rs delete mode 100644 src/shell_injection/is_safely_encapsulated_test.rs delete mode 100644 src/shell_injection/mod.rs diff --git a/README.md b/README.md index 1c595db..d731079 100644 --- a/README.md +++ b/README.md @@ -14,13 +14,16 @@ import ctypes zen_internals = ctypes.CDLL("target/release/libzen_internals.so") if __name__ == "__main__": - command = "whoami | shell".encode("utf-8") - userinput = "whoami".encode("utf-8") - result = zen_internals.detect_shell_injection(command, userinput) + query = "SELECT * FROM users WHERE id = '' OR 1=1 -- '".encode("utf-8") + userinput = "' OR 1=1 -- ".encode("utf-8") + dialect = 9 # MySQL dialect + result = zen_internals.detect_sql_injection(command, userinput, dialect) print("Result", bool(result)) ``` -## Node.js bindings +See [list of dialects](https://github.com/AikidoSec/zen-internals/blob/main/src/sql_injection/helpers/select_dialect_based_on_enum.rs#L18) + +## Node.js bindings (using WASM) ### Install diff --git a/smoketests/wasm.js b/smoketests/wasm.js index 44ccd0b..8e60096 100644 --- a/smoketests/wasm.js +++ b/smoketests/wasm.js @@ -3,7 +3,7 @@ const { equal } = require("node:assert"); equal(internals.wasm_detect_sql_injection("SELECT * FROM users WHERE id = '' OR 1=1 -- '", "' OR 1=1 -- ", 0), true); -equal(internals.wasm_detect_shell_injection("SELECT * FROM users WHERE id = 'hello world'", 'hello world'), false); +equal(internals.wasm_detect_sql_injection("SELECT * FROM users WHERE id = 'hello world'", 'hello world'), false); equal(internals.wasm_detect_js_injection("const test = 'Hello World!'; //';", "Hello World!'; //", 0), true); diff --git a/src/ffi_bindings/mod.rs b/src/ffi_bindings/mod.rs index 2f4460d..0c04c11 100644 --- a/src/ffi_bindings/mod.rs +++ b/src/ffi_bindings/mod.rs @@ -1,38 +1,10 @@ use crate::js_injection::detect_js_injection::detect_js_injection_str; -use crate::shell_injection::detect_shell_injection::detect_shell_injection_stringified; use crate::sql_injection::detect_sql_injection::detect_sql_injection_str; use std::ffi::CStr; use std::os::raw::{c_char, c_int}; use std::panic; use std::str; -#[no_mangle] -pub extern "C" fn detect_shell_injection( - command: *const c_char, - userinput: *const c_char, -) -> c_int { - // Returns an integer value, representing a boolean (1 = true, 0 = false, 2 = error) - return panic::catch_unwind(|| { - // Check if the pointers are null - if command.is_null() || userinput.is_null() { - return 2; - } - - let command_bytes = unsafe { CStr::from_ptr(command).to_bytes() }; - let userinput_bytes = unsafe { CStr::from_ptr(userinput).to_bytes() }; - - let command_str = str::from_utf8(command_bytes).unwrap(); - let userinput_str = str::from_utf8(userinput_bytes).unwrap(); - - if detect_shell_injection_stringified(command_str, userinput_str) { - return 1; - } - - return 0; - }) - .unwrap_or(2); -} - #[no_mangle] pub extern "C" fn detect_sql_injection( query: *const c_char, diff --git a/src/lib.rs b/src/lib.rs index e4be83e..fc1c89f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,6 @@ * - SQL Injection */ mod helpers; -mod shell_injection; // FFI Bindings pub mod ffi_bindings; diff --git a/src/shell_injection/contains_shell_syntax.rs b/src/shell_injection/contains_shell_syntax.rs deleted file mode 100644 index aa749fe..0000000 --- a/src/shell_injection/contains_shell_syntax.rs +++ /dev/null @@ -1,174 +0,0 @@ -use regex::{Regex, RegexBuilder}; - -// Constants -const DANGEROUS_CHARS: [&str; 26] = [ - "#", "!", "\"", "$", "&", "'", "(", ")", "*", ";", "<", "=", ">", "?", "[", "\\", "]", "^", - "`", "{", "|", "}", " ", "\n", "\t", "~", -]; -const COMMANDS: [&str; 61] = [ - "sleep", - "shutdown", - "reboot", - "poweroff", - "halt", - "ifconfig", - "chmod", - "chown", - "ping", - "ssh", - "scp", - "curl", - "wget", - "telnet", - "kill", - "killall", - "rm", - "mv", - "cp", - "touch", - "echo", - "cat", - "head", - "tail", - "grep", - "find", - "awk", - "sed", - "sort", - "uniq", - "wc", - "ls", - "env", - "ps", - "who", - "whoami", - "id", - "w", - "df", - "du", - "pwd", - "uname", - "hostname", - "netstat", - "passwd", - "arch", - "printenv", - "logname", - "pstree", - "hostnamectl", - "set", - "lsattr", - "killall5", - "dmesg", - "history", - "free", - "uptime", - "finger", - "top", - "shopt", - ":", // Colon is a null command -]; -const PATH_PREFIXES: [&str; 6] = [ - "/bin/", - "/sbin/", - "/usr/bin/", - "/usr/sbin/", - "/usr/local/bin/", - "/usr/local/sbin/", -]; -const SEPARATORS: [&str; 10] = [" ", "\t", "\n", ";", "&", "|", "(", ")", "<", ">"]; - -fn create_commands_regex() -> Regex { - // Escape path prefixes and join them - let path_prefixes_pattern = PATH_PREFIXES - .iter() - .map(|s| regex::escape(s)) - .collect::>() - .join("|"); - - // Sort commands by length in descending order and escape them - let mut sorted_commands = COMMANDS.to_vec(); - sorted_commands.sort_by_key(|b| std::cmp::Reverse(b.len())); // Sort by length, descending - let commands_pattern = sorted_commands - .iter() - .map(|s| regex::escape(s)) - .collect::>() - .join("|"); - - // Create the regex pattern - let pattern = format!(r"([/.]*({})?({}))", path_prefixes_pattern, commands_pattern); - - // Create the regex with case insensitive and multiline flags - RegexBuilder::new(&pattern) - .case_insensitive(true) - .multi_line(true) - .build() - .unwrap() -} - -// Function to check if the user input contains shell syntax given the command -pub fn contains_shell_syntax(command: &str, user_input: &str) -> bool { - let commands_regex = create_commands_regex(); - - if user_input.trim().is_empty() { - // The entire user input is just whitespace, ignore - return false; - } - - if DANGEROUS_CHARS.iter().any(|&c| user_input.contains(c)) { - return true; - } - - // The command is the same as the user input - if command == user_input { - // Check if the command matches the regex - if let Some(m) = commands_regex.find(command) { - return m.start() == 0 && m.end() == command.len(); - } - return false; - } - - // Check if the command contains a commonly used command - for mat in commands_regex.captures_iter(command) { - let matched_command = &mat[0]; - // We found a command like `rm` or `/sbin/shutdown` in the command - // Check if the command is the same as the user input - if user_input != matched_command { - continue; - } - - // Check surrounding characters - let start_index = mat.get(0).unwrap().start(); - let end_index = mat.get(0).unwrap().end(); - - let char_before = if start_index > 0 { - command.chars().nth(start_index - 1) - } else { - None - }; - - let char_after = if end_index < command.len() { - command.chars().nth(end_index) - } else { - None - }; - - // Check surrounding characters - if char_before.map_or(false, |c| SEPARATORS.contains(&c.to_string().as_str())) - && char_after.map_or(false, |c| SEPARATORS.contains(&c.to_string().as_str())) - { - return true; // e.g. `rm` - } - if char_before.map_or(false, |c| SEPARATORS.contains(&c.to_string().as_str())) - && char_after.is_none() - { - return true; // e.g. `rm` - } - if char_before.is_none() - && char_after.map_or(false, |c| SEPARATORS.contains(&c.to_string().as_str())) - { - return true; // e.g. `rm` - } - } - false -} diff --git a/src/shell_injection/contains_shell_syntax_test.rs b/src/shell_injection/contains_shell_syntax_test.rs deleted file mode 100644 index a73cc68..0000000 --- a/src/shell_injection/contains_shell_syntax_test.rs +++ /dev/null @@ -1,171 +0,0 @@ -#[cfg(test)] -mod tests { - use crate::shell_injection::contains_shell_syntax::contains_shell_syntax; - #[test] - fn test_detects_shell_syntax() { - assert_eq!(contains_shell_syntax("", ""), false); - assert_eq!(contains_shell_syntax("hello", "hello"), false); - assert_eq!(contains_shell_syntax("\n", "\n"), false); - assert_eq!(contains_shell_syntax("\n\n", "\n\n"), false); - - assert_eq!(contains_shell_syntax("$(command)", "$(command)"), true); - assert_eq!( - contains_shell_syntax("$(command arg arg)", "$(command arg arg"), - true - ); - assert_eq!(contains_shell_syntax("`command`", "`command`"), true); - assert_eq!(contains_shell_syntax("\narg", "\narg"), true); - assert_eq!(contains_shell_syntax("\targ", "\targ"), true); - - assert_eq!(contains_shell_syntax("\narg\n", "\narg\n"), true); - assert_eq!(contains_shell_syntax("arg\n", "arg\n"), true); - assert_eq!(contains_shell_syntax("arg\narg", "arg\narg"), true); - assert_eq!(contains_shell_syntax("rm -rf", "rm -rf"), true); - assert_eq!(contains_shell_syntax("/bin/rm -rf", "/bin/rm -rf"), true); - assert_eq!(contains_shell_syntax("/bin/rm", "/bin/rm"), true); - assert_eq!(contains_shell_syntax("/sbin/sleep", "/sbin/sleep"), true); - assert_eq!( - contains_shell_syntax("/usr/bin/kill", "/usr/bin/kill"), - true - ); - - assert_eq!( - contains_shell_syntax("/usr/bin/killall", "/usr/bin/killall"), - true - ); - assert_eq!(contains_shell_syntax("/usr/bin/env", "/usr/bin/env"), true); - assert_eq!(contains_shell_syntax("/bin/ps", "/bin/ps"), true); - //assert_eq!(contains_shell_syntax("/usr/bin/W", "/usr/bin/W"), true); - assert_eq!(contains_shell_syntax("lsattr", "lsattr"), true); - } - #[test] - fn test_detects_commands_surrounded_by_separators() { - assert_eq!( - contains_shell_syntax( - r#"find /path/to/search -type f -name "pattern" -exec rm {} \; "#, - "rm" - ), - true - ); - } - - #[test] - fn test_detects_commands_with_separator_before() { - assert_eq!( - contains_shell_syntax( - r#"find /path/to/search -type f -name "pattern" | xargs rm"#, - "rm" - ), - true - ); - } - - #[test] - fn test_detects_commands_with_separator_after() { - assert!(contains_shell_syntax("rm arg", "rm")); - } - - #[test] - fn test_checks_if_same_command_occurs_in_user_input() { - assert!(!contains_shell_syntax("find cp", "rm")); - } - - #[test] - fn test_treats_colon_as_command() { - assert!(contains_shell_syntax(":|echo", ":|")); - assert!(!contains_shell_syntax( - "https://www.google.com", - "https://www.google.com" - )); - } - - #[test] - fn test_detects_commands_with_separators() { - assert!(contains_shell_syntax("rm>arg", "rm")); - assert!(contains_shell_syntax("rm bool { - if user_input == "~" && command.len() > 1 && command.contains("~") { - // Block single ~ character. For example echo ~ - return true; - } - - if user_input.len() <= 1 { - // We ignore single characters since they don't pose a big threat. - // They are only able to crash the shell, not execute arbitrary commands. - return false; - } - - if user_input.len() > command.len() { - // We ignore cases where the user input is longer than the command. - // Because the user input can't be part of the command. - return false; - } - - if !command.contains(user_input) { - return false; - } - - if is_safely_encapsulated(command, user_input) { - return false; - } - - contains_shell_syntax(command, user_input) -} diff --git a/src/shell_injection/detect_shell_injection_test.rs b/src/shell_injection/detect_shell_injection_test.rs deleted file mode 100644 index 4b0c289..0000000 --- a/src/shell_injection/detect_shell_injection_test.rs +++ /dev/null @@ -1,454 +0,0 @@ -#[cfg(test)] -mod tests { - use crate::shell_injection::detect_shell_injection::detect_shell_injection_stringified; - use std::assert; - - fn is_shell_injection(command: &str, user_input: &str) { - assert!( - detect_shell_injection_stringified(command, user_input), - "command: {}, userInput: {}", - command, - user_input - ); - } - - fn is_not_shell_injection(command: &str, user_input: &str) { - assert!( - !detect_shell_injection_stringified(command, user_input), - "command: {}, userInput: {}", - command, - user_input - ); - } - - #[test] - fn test_single_characters_ignored() { - is_not_shell_injection("ls `", "`"); - is_not_shell_injection("ls *", "*"); - is_not_shell_injection("ls a", "a"); - } - - #[test] - fn test_no_user_input() { - is_not_shell_injection("ls", ""); - is_not_shell_injection("ls", " "); - is_not_shell_injection("ls", " "); - is_not_shell_injection("ls", " "); - } - - #[test] - fn test_user_input_not_in_command() { - is_not_shell_injection("ls", "$(echo)"); - } - - #[test] - fn test_user_input_longer_than_command() { - is_not_shell_injection("`ls`", "`ls` `ls`"); - } - - #[test] - fn test_detects_command_substitution() { - is_shell_injection("ls $(echo)", "$(echo)"); - is_shell_injection("ls \"$(echo)\"", "$(echo)"); - is_shell_injection( - "echo $(echo \"Inner: $(echo \"This is nested\")\")", - "$(echo \"Inner: $(echo \"This is nested\")\")", - ); - - is_not_shell_injection("ls '$(echo)'", "$(echo)"); - is_not_shell_injection( - "ls '$(echo \"Inner: $(echo \"This is nested\")\")'", - "$(echo \"Inner: $(echo \"This is nested\")\")", - ); - } - - #[test] - fn test_detects_backticks() { - is_shell_injection("echo `echo`", "`echo`"); - } - - #[test] - fn test_checks_unsafely_quoted() { - is_shell_injection("ls '$(echo)", "$(echo)"); - } - - #[test] - fn test_single_quote_between_single_quotes() { - is_shell_injection("ls ''single quote''", "'single quote'"); - } - - #[test] - fn test_no_special_chars_inside_double_quotes() { - is_shell_injection("ls \"whatever$\"", "whatever$"); - is_shell_injection("ls \"whatever!\"", "whatever!"); - is_shell_injection("ls \"whatever`\"", "whatever`"); - } - - #[test] - fn test_no_semi_colon() { - is_shell_injection("ls whatever;", "whatever;"); - is_not_shell_injection("ls \"whatever;\"", "whatever;"); - is_not_shell_injection("ls 'whatever;'", "whatever;"); - } - - #[test] - fn test_rm_rf_executed_by_using_semi_colon() { - is_shell_injection("ls; rm -rf", "; rm -rf"); - } - - #[test] - fn test_rm_rf_is_flagged_as_shell_injection() { - is_shell_injection("rm -rf", "rm -rf"); - } - - #[test] - fn test_shell_injection_with_chained_commands_using_and() { - is_shell_injection("ls && rm -rf /", "&& rm -rf /"); - } - - #[test] - fn test_shell_injection_with_or_logic() { - is_shell_injection("ls || echo 'malicious code'", "|| echo 'malicious code'"); - } - - #[test] - fn test_detects_redirection_attempts() { - is_shell_injection("ls > /dev/null", "> /dev/null"); - is_shell_injection("cat file.txt > /etc/passwd", "> /etc/passwd"); - } - - #[test] - fn test_detects_append_redirection_attempts() { - is_shell_injection("echo 'data' >> /etc/passwd", ">> /etc/passwd"); - } - - #[test] - fn test_detects_pipe_character_as_shell_injection() { - is_shell_injection("cat file.txt | grep 'password'", "| grep 'password'"); - } - - #[test] - fn test_allows_safe_use_of_pipe_character_within_quotes() { - is_not_shell_injection("echo '|'", "|"); - } - - #[test] - fn test_detects_nested_command_substitution() { - is_shell_injection("echo $(cat $(ls))", "$(cat $(ls))"); - } - - #[test] - fn test_allows_safe_commands_within_single_quotes() { - is_not_shell_injection("echo 'safe command'", "safe command"); - } - - #[test] - fn test_detects_unsafe_use_of_variables() { - is_shell_injection("echo $USER", "$USER"); - is_shell_injection("echo ${USER}", "${USER}"); - is_shell_injection("echo \"${USER}\"", "${USER}"); - } - - #[test] - fn test_allows_safe_use_of_variables_within_quotes() { - is_not_shell_injection("echo '$USER'", "$USER"); - } - - #[test] - fn test_detects_subshell_execution_with_backticks_inside_double_quotes() { - is_shell_injection("ls \"$(echo `whoami`)", "`whoami`"); - } - - #[test] - fn test_detects_code_injection_with_newline_characters() { - is_shell_injection("echo 'safe'\necho 'malicious'", "\necho 'malicious'"); - } - - #[test] - fn test_detects_attempts_to_escape_out_of_quotes() { - is_shell_injection( - "echo \"safe\"; echo \"malicious\"", - "\"; echo \"malicious\"", - ); - } - - #[test] - fn test_correctly_handles_whitespace_in_inputs() { - is_not_shell_injection("ls", " "); - is_shell_injection("ls ; rm -rf /", "; rm -rf /"); - } - - #[test] - fn test_detects_file_manipulation_commands() { - is_shell_injection("touch /tmp/malicious", "touch /tmp/malicious"); - is_shell_injection("mv /tmp/safe /tmp/malicious", "mv /tmp/safe /tmp/malicious"); - } - - #[test] - fn test_allows_commands_with_constants_that_resemble_user_input() { - is_not_shell_injection("echo 'userInput'", "userInput"); - } - - #[test] - fn test_recognizes_safe_paths_that_include_patterns_similar_to_user_input() { - is_not_shell_injection( - "ls /constant/path/without/user/input/", - "/constant/path/without/user/input/", - ); - } - - #[test] - fn test_acknowledges_safe_use_of_special_characters_when_properly_encapsulated() { - is_not_shell_injection("echo ';'", ";"); - is_not_shell_injection("echo '&&'", "&&"); - is_not_shell_injection("echo '||'", "||"); - } - - #[test] - fn test_treats_encapsulated_redirection_and_pipe_symbols_as_safe() { - is_not_shell_injection("echo 'data > file.txt'", "data > file.txt"); - is_not_shell_injection("echo 'find | grep'", "find | grep"); - } - - #[test] - fn test_recognizes_safe_inclusion_of_special_patterns_within_quotes_as_non_injections() { - is_not_shell_injection("echo '$(command)'", "$(command)"); - } - - #[test] - fn test_considers_constants_with_semicolons_as_safe_when_non_executable() { - is_not_shell_injection("echo 'text; more text'", "text; more text"); - } - - #[test] - fn test_acknowledges_commands_that_look_dangerous_but_are_safe_due_to_quoting() { - is_not_shell_injection("echo '; rm -rf /'", "; rm -rf /"); - is_not_shell_injection("echo '&& echo malicious'", "&& echo malicious"); - } - - #[test] - fn test_recognizes_commands_with_newline_characters_as_safe_when_encapsulated() { - is_not_shell_injection("echo 'line1\nline2'", "line1\nline2"); - } - - #[test] - fn test_accepts_special_characters_in_constants_as_safe_when_no_execution() { - is_not_shell_injection("echo '*'", "*"); - is_not_shell_injection("echo '?'", "?"); - is_not_shell_injection("echo '\\' ", "\\"); - } - - #[test] - fn test_does_not_flag_command_with_matching_whitespace_as_injection() { - is_not_shell_injection("ls -l", " "); // A single space is just an argument separator, not user input - } - - #[test] - fn test_ignores_commands_where_multiple_spaces_match_user_input() { - is_not_shell_injection("ls -l", " "); // Multiple spaces between arguments should not be considered injection - } - - #[test] - fn test_does_not_consider_leading_whitespace_in_commands_as_user_input() { - is_not_shell_injection(" ls -l", " "); // Leading spaces before the command are not user-controlled - } - - #[test] - fn test_treats_trailing_whitespace_in_commands_as_non_injection() { - is_not_shell_injection("ls -l ", " "); // Trailing space after the command is benign - } - - #[test] - fn test_recognizes_spaces_between_quotes_as_non_injective() { - is_not_shell_injection("echo ' ' ", " "); // Space within quotes is part of the argument, not a separator - } - - #[test] - fn test_handles_spaces_within_quoted_arguments_correctly() { - is_not_shell_injection("command 'arg with spaces'", " "); // Spaces within a quoted argument should not be flagged - } - - #[test] - fn test_correctly_interprets_spaces_in_mixed_argument_types() { - is_not_shell_injection("command arg1 'arg with spaces' arg2", " "); // Mixed argument types with internal spaces are safe - } - - #[test] - fn test_ignores_spaces_in_commands_with_concatenated_arguments() { - is_not_shell_injection("command 'arg1'arg2'arg3'", " "); // Lack of spaces in concatenated arguments is intentional and safe - } - - #[test] - fn test_does_not_flag_spaces_in_commands_with_no_arguments() { - is_not_shell_injection("command", " "); // No arguments mean spaces are irrelevant - } - - #[test] - fn test_considers_spaces_in_environment_variable_assignments_as_safe() { - is_not_shell_injection("ENV_VAR='value' command", " "); // Spaces around environment variable assignments are not injections - } - - #[test] - fn test_new_lines_in_commands_are_considered_injections() { - is_shell_injection("ls \nrm", "\nrm"); - is_shell_injection("ls \nrm -rf", "\nrm -rf"); - } - - #[test] - fn test_new_lines_alone_are_not_considered_injections() { - is_not_shell_injection("ls\n\n", "\n\n"); - } - - #[test] - fn test_certain_commands_are_always_flagged_as_dangerous() { - is_shell_injection("/bin/rm -rf", "/bin/rm -rf"); - is_shell_injection("rm -rf", "rm -rf"); - is_shell_injection("rm -rf /", "rm -rf /"); - is_shell_injection("sleep 10", "sleep 10"); - is_shell_injection("sleep 10 &", "sleep 10 &"); - is_shell_injection("shutdown -h now", "shutdown -h now"); - is_shell_injection("halt", "halt"); - is_shell_injection("poweroff", "poweroff"); - is_shell_injection("reboot", "reboot"); - is_shell_injection("reboot -f", "reboot -f"); - is_shell_injection("ifconfig", "ifconfig"); - is_shell_injection("ifconfig -a", "ifconfig -a"); - is_shell_injection("kill", "kill"); - is_shell_injection("killall", "killall"); - is_shell_injection("killall -9", "killall -9"); - is_shell_injection("chmod", "chmod"); - is_shell_injection("chmod 777", "chmod 777"); - is_shell_injection("chown", "chown"); - is_shell_injection("chown root", "chown root"); - } - - #[test] - fn test_rm_being_part_of_other_commands() { - is_shell_injection( - "find /path/to/search -type f -name \"pattern\" | xargs rm", - "rm", - ); - is_shell_injection( - "find /path/to/search -type f -name \"pattern\" -exec rm {} \\;", - "rm", - ); - is_shell_injection("ls .|rm", "rm"); - } - - #[test] - fn test_ignores_dangerous_commands_if_part_of_string() { - is_not_shell_injection("binary sleepwithme", "sleepwithme"); - is_not_shell_injection("binary rm-rf", "rm-rf"); - is_not_shell_injection("term", "term"); - is_not_shell_injection("rm /files/rm.txt", "rm.txt"); - } - - #[test] - fn test_does_not_flag_domain_name_as_argument_unless_contains_backticks() { - is_not_shell_injection("binary --domain www.example.com", "www.example.com"); - is_not_shell_injection( - "binary --domain https://www.example.com", - "https://www.example.com", - ); - - is_shell_injection( - "binary --domain www.example`whoami`.com", - "www.example`whoami`.com", - ); - is_shell_injection( - "binary --domain https://www.example`whoami`.com", - "https://www.example`whoami`.com", - ); - } - - #[test] - fn test_flags_colon_if_used_as_command() { - is_shell_injection(":|echo", ":|"); - is_shell_injection(":| echo", ":|"); - is_shell_injection(": | echo", ": |"); - } - - #[test] - fn test_detects_shell_injection() { - is_shell_injection("/usr/bin/kill", "/usr/bin/kill"); - } - - #[test] - fn test_detects_shell_injection_with_uppercase_path() { - is_shell_injection("/usr/bIn/kill", "/usr/bIn/kill"); - } - - #[test] - fn test_detects_shell_injection_with_uppercase_command() { - is_shell_injection("/bin/CAT", "/bin/CAT"); - } - - #[test] - fn test_detects_shell_injection_with_uppercase_path_and_command() { - is_shell_injection("/bIn/LS -la", "/bIn/LS -la"); - } - - #[test] - fn test_shell_injection_with_multiple_slashes() { - is_shell_injection("//bin/ls", "//bin/ls"); - is_shell_injection("///bin/ls", "///bin/ls"); - } - - #[test] - fn test_shell_injection_with_dotdot() { - is_shell_injection("../bin/ls", "../bin/ls"); - is_shell_injection("../../bin/ls", "../../bin/ls"); - is_shell_injection("/../bin/ls", "/../bin/ls"); - is_shell_injection("/./bin/ls", "/./bin/ls"); - } - - #[test] - fn test_shell_injection_with_tilde() { - is_shell_injection("echo ~", "~"); - is_shell_injection("ls ~/.ssh", "~/.ssh"); - } - - #[test] - fn test_no_shell_injection_with_tilde() { - is_not_shell_injection("~", "~"); - is_not_shell_injection("ls ~/path", "path"); - } - - #[test] - fn test_real_case() { - is_shell_injection( - "command -disable-update-check -target https://examplx.com|curl+https://cde-123.abc.domain.com+%23 -json-export /tmp/5891/8526757.json -tags microsoft,windows,exchange,iis,gitlab,oracle,cisco,joomla -stats -stats-interval 3 -retries 3 -no-stdin", - "https://examplx.com|curl+https://cde-123.abc.domain.com+%23", - ); - } - - #[test] - fn test_false_positive_with_email() { - is_not_shell_injection( - "echo token | docker login --username john.doe@acme.com --password-stdin hub.acme.com", - "john.doe@acme.com", - ); - } - - #[test] - fn test_at_sign_with_shell_syntax() { - is_shell_injection("'echo \"${array[@]}\"'", "${array[@]}"); - is_shell_injection("echo $@", "$@"); - } - - #[test] - fn test_allows_comma_separated_list() { - is_not_shell_injection( - "command -tags php,laravel,drupal,phpmyadmin,symfony -stats", - "php,laravel,drupal,phpmyadmin,symfony", - ); - } - - #[test] - fn test_it_flags_comma_in_loop() { - is_shell_injection( - "command for (( i=0, j=10; i) -> Vec<(&str, &str)> { - // Create a vector to hold the segments - let mut segments = Vec::new(); - - // Iterate over the indices of the array, stopping before the last element - for i in 0..array.len() - 1 { - // Push a tuple of the current and next elements into the segments vector - segments.push((array[i], array[i + 1])); - } - - segments -} - -// Constants for escape characters and dangerous characters -const ESCAPE_CHARS: [&str; 2] = ["\"", "'"]; -const DANGEROUS_CHARS_INSIDE_DOUBLE_QUOTES: [&str; 4] = ["$", "`", "\\", "!"]; - -// Function to check if user input is safely encapsulated -pub fn is_safely_encapsulated(command: &str, user_input: &str) -> bool { - let segments = get_current_and_next_segments(command.split(user_input).collect()); - - for (current_segment, next_segment) in segments { - let char_before_user_input = current_segment.chars().last(); - let char_after_user_input = next_segment.chars().next(); - - let is_escape_char = char_before_user_input - .map_or(false, |c| ESCAPE_CHARS.contains(&c.to_string().as_str())); - - if !is_escape_char { - return false; - } - - if char_before_user_input != char_after_user_input { - return false; - } - - if char_before_user_input.map_or(false, |c| user_input.contains(c)) { - return false; - } - - // Check for dangerous characters inside double quotes - if char_before_user_input == Some('"') { - if user_input - .chars() - .any(|c| DANGEROUS_CHARS_INSIDE_DOUBLE_QUOTES.contains(&c.to_string().as_str())) - { - return false; - } - } - } - - true -} diff --git a/src/shell_injection/is_safely_encapsulated_test.rs b/src/shell_injection/is_safely_encapsulated_test.rs deleted file mode 100644 index 00e0312..0000000 --- a/src/shell_injection/is_safely_encapsulated_test.rs +++ /dev/null @@ -1,55 +0,0 @@ -#[cfg(test)] -mod tests { - use crate::shell_injection::is_safely_encapsulated::is_safely_encapsulated; - use std::assert_eq; - - #[test] - fn test_safe_between_single_quotes() { - assert_eq!(is_safely_encapsulated("echo '$USER'", "$USER"), true); - assert_eq!(is_safely_encapsulated("echo '`$USER'", "`USER"), true); - } - #[test] - fn test_single_quote_in_single_quotes() { - assert_eq!(is_safely_encapsulated("echo ''USER'", "'USER"), false); - } - #[test] - fn test_dangerous_chars_between_double_quotes() { - assert_eq!(is_safely_encapsulated("echo \"=USER\"", "=USER"), true); - assert_eq!(is_safely_encapsulated("echo \"$USER\"", "$USER"), false); - assert_eq!(is_safely_encapsulated("echo \"!USER\"", "!USER"), false); - assert_eq!(is_safely_encapsulated("echo \"\\`USER\"", "`USER"), false); - assert_eq!(is_safely_encapsulated("echo \"\\USER\"", "\\USER"), false); - } - #[test] - fn test_same_user_input_multiple_times() { - assert_eq!( - is_safely_encapsulated("echo '$USER' '$USER'", "$USER"), - true - ); - assert_eq!( - is_safely_encapsulated("echo \"$USER\" '$USER'", "$USER"), - false - ); - assert_eq!( - is_safely_encapsulated("echo \"$USER\" \"$USER\"", "$USER"), - false - ); - } - - #[test] - fn test_first_and_last_quote_does_not_match() { - assert_eq!(is_safely_encapsulated("echo '$USER\"", "$USER"), false); - assert_eq!(is_safely_encapsulated("echo \"$USER'", "$USER"), false); - } - - #[test] - fn test_first_or_last_character_not_escape_char() { - assert_eq!(is_safely_encapsulated("echo $USER'", "$USER"), false); - assert_eq!(is_safely_encapsulated("echo $USER\"", "$USER"), false); - } - #[test] - fn test_user_input_does_not_occur_in_command() { - assert_eq!(is_safely_encapsulated("echo 'USER'", "$USER"), true); - assert_eq!(is_safely_encapsulated("echo \"USER\"", "$USER"), true); - } -} diff --git a/src/shell_injection/mod.rs b/src/shell_injection/mod.rs deleted file mode 100644 index 49f3989..0000000 --- a/src/shell_injection/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub mod contains_shell_syntax; -pub mod contains_shell_syntax_test; - -pub mod detect_shell_injection; -pub mod detect_shell_injection_test; - -pub mod is_safely_encapsulated; -pub mod is_safely_encapsulated_test; diff --git a/src/wasm_bindings/mod.rs b/src/wasm_bindings/mod.rs index 81dff99..968c74d 100644 --- a/src/wasm_bindings/mod.rs +++ b/src/wasm_bindings/mod.rs @@ -1,13 +1,7 @@ use crate::js_injection::detect_js_injection::detect_js_injection_str; -use crate::shell_injection::detect_shell_injection::detect_shell_injection_stringified; use crate::sql_injection::detect_sql_injection::detect_sql_injection_str; use wasm_bindgen::prelude::*; -#[wasm_bindgen] -pub fn wasm_detect_shell_injection(command: &str, userinput: &str) -> bool { - detect_shell_injection_stringified(command, userinput) -} - #[wasm_bindgen] pub fn wasm_detect_sql_injection(query: &str, userinput: &str, dialect: i32) -> bool { detect_sql_injection_str(query, userinput, dialect)