diff --git a/aikido_zen/vulnerabilities/shell_injection/contains_shell_syntax.py b/aikido_zen/vulnerabilities/shell_injection/contains_shell_syntax.py index 3d590659d..71b48a7bc 100644 --- a/aikido_zen/vulnerabilities/shell_injection/contains_shell_syntax.py +++ b/aikido_zen/vulnerabilities/shell_injection/contains_shell_syntax.py @@ -109,6 +109,17 @@ separators = [" ", "\t", "\n", ";", "&", "|", "(", ")", "<", ">", "\r", "\f"] +# Patterns that can expand to create separators at runtime +# These are shell expansion patterns that can be used to bypass literal separator checks +expansion_patterns = [ + "${IFS}", # Internal Field Separator (whitespace by default) + "$IFS", # IFS without braces + "${IFS%?}", # IFS with parameter expansion (removes shortest suffix) + "${IFS#?}", # IFS with parameter expansion (removes shortest prefix) + "${IFS%%?}", # IFS with parameter expansion (removes longest suffix) + "${IFS##?}", # IFS with parameter expansion (removes longest prefix) +] + # Function to sort commands by length (longer commands first) def by_length(a, b): @@ -138,6 +149,12 @@ def contains_shell_syntax(command, user_input): if any(c in user_input for c in dangerous_chars): return True + # Check if user input contains shell expansion patterns that can create separators + # e.g., ${IFS}, $IFS which expand to whitespace at runtime + for pattern in expansion_patterns: + if pattern in user_input: + return True + # The command is the same as the user input # Rare case, but it's possible # e.g. command is `shutdown` and user input is `shutdown` @@ -162,6 +179,44 @@ def contains_shell_syntax(command, user_input): char_before = command[match.start() - 1] if match.start() > 0 else None char_after = command[match.end()] if match.end() < len(command) else None + # Check if preceded by an expansion pattern that can create a separator + # e.g. `true;${IFS}whoami` where ${IFS} expands to whitespace + if match.start() > 0: + for pattern in expansion_patterns: + pattern_start = match.start() - len(pattern) + if pattern_start >= 0: + preceding_text = command[pattern_start : match.start()] + if preceding_text == pattern: + # Found an expansion pattern immediately before the matched command + # Check if there's a separator before the expansion pattern + char_before_expansion = ( + command[pattern_start - 1] if pattern_start > 0 else None + ) + if ( + char_before_expansion in separators + or char_before_expansion is None + ): + return True + + # Check if followed by an expansion pattern that can create a separator + # e.g. `whoami${IFS}echo` where ${IFS} expands to whitespace + if match.end() < len(command): + for pattern in expansion_patterns: + pattern_end = match.end() + len(pattern) + if pattern_end <= len(command): + following_text = command[match.end() : pattern_end] + if following_text == pattern: + # Found an expansion pattern immediately after the matched command + # Check if there's a separator after the expansion pattern + char_after_expansion = ( + command[pattern_end] if pattern_end < len(command) else None + ) + if ( + char_after_expansion in separators + or char_after_expansion is None + ): + return True + # Check surrounding characters if char_before in separators and char_after in separators: # e.g. `rm` diff --git a/aikido_zen/vulnerabilities/shell_injection/contains_shell_syntax_test.py b/aikido_zen/vulnerabilities/shell_injection/contains_shell_syntax_test.py index 785d470c8..d5d0a49bd 100644 --- a/aikido_zen/vulnerabilities/shell_injection/contains_shell_syntax_test.py +++ b/aikido_zen/vulnerabilities/shell_injection/contains_shell_syntax_test.py @@ -161,3 +161,30 @@ def test_form_feed_as_separator(): assert contains_shell_syntax("ls\frm", "rm") is True assert contains_shell_syntax("echo test\frm -rf /", "rm") is True assert contains_shell_syntax("rm\fls", "rm") is True + + +def test_detects_shell_expansion_separators(): + # Test ${IFS} expansion that creates runtime separators + assert contains_shell_syntax("true;${IFS}whoami", "whoami") is True + assert contains_shell_syntax("echo${IFS}whoami", "whoami") is True + assert contains_shell_syntax("ls;${IFS}rm", "rm") is True + + # Test $IFS expansion (without braces) + assert contains_shell_syntax("true;$IFS whoami", "whoami") is True + assert contains_shell_syntax("echo$IFS rm", "rm") is True + + # Test IFS with parameter expansion (various forms) + assert contains_shell_syntax("true;${IFS%?}whoami", "whoami") is True + assert contains_shell_syntax("true;${IFS#?}whoami", "whoami") is True + assert contains_shell_syntax("true;${IFS%%?}whoami", "whoami") is True + assert contains_shell_syntax("true;${IFS##?}whoami", "whoami") is True + + # Test expansion patterns in user input itself + assert contains_shell_syntax("cmd ${IFS}whoami", "${IFS}whoami") is True + assert contains_shell_syntax("cmd $IFS", "$IFS") is True + assert contains_shell_syntax("cmd ${IFS%?}", "${IFS%?}") is True + + # Test expansion after command + assert contains_shell_syntax("whoami${IFS}echo", "whoami") is True + assert contains_shell_syntax("rm${IFS%?}ls", "rm") is True + assert contains_shell_syntax("rm${IFS##?}ls", "rm") is True diff --git a/aikido_zen/vulnerabilities/shell_injection/detect_shell_injection_test.py b/aikido_zen/vulnerabilities/shell_injection/detect_shell_injection_test.py index cb264f2f0..c2f5fa924 100644 --- a/aikido_zen/vulnerabilities/shell_injection/detect_shell_injection_test.py +++ b/aikido_zen/vulnerabilities/shell_injection/detect_shell_injection_test.py @@ -426,3 +426,25 @@ def test_form_feed_as_separator_between_commands(): is_shell_injection("ls\frm", "rm") is_shell_injection("echo test\frm -rf /", "rm") is_shell_injection("rm\fls", "rm") + + +def test_detects_shell_expansion_bypass(): + # Test the specific bypass case from the security report + is_shell_injection("true;${IFS}whoami", "whoami") + + # Test various IFS expansion patterns + is_shell_injection("echo${IFS}whoami", "whoami") + is_shell_injection("ls;${IFS}rm", "rm") + is_shell_injection("true;$IFS whoami", "whoami") + is_shell_injection("cmd;${IFS%?}whoami", "whoami") + is_shell_injection("cmd;${IFS#?}whoami", "whoami") + is_shell_injection("cmd;${IFS%%?}whoami", "whoami") + is_shell_injection("cmd;${IFS##?}whoami", "whoami") + + # Test expansion patterns in user input + is_shell_injection("cmd ${IFS}whoami", "${IFS}whoami") + is_shell_injection("cmd $IFS", "$IFS") + + # Test expansion after command + is_shell_injection("whoami${IFS}echo", "whoami") + is_shell_injection("rm${IFS%?}ls", "rm")