#!/usr/bin/env python """ Security test for prowler-wrapper.py command injection vulnerability This test demonstrates the command injection vulnerability and validates the fix """ import os import shutil import sys import tempfile import unittest from unittest.mock import MagicMock, patch class TestProwlerWrapperSecurity(unittest.TestCase): """Test cases for command injection vulnerability in prowler-wrapper.py""" def setUp(self): """Set up test environment""" # Create a temporary directory for testing self.test_dir = tempfile.mkdtemp() self.prowler_wrapper_path = os.path.join( os.path.dirname( os.path.dirname( os.path.dirname(os.path.dirname(os.path.abspath(__file__))) ) ), "contrib", "wazuh", "prowler-wrapper.py", ) def tearDown(self): """Clean up test environment""" shutil.rmtree(self.test_dir, ignore_errors=True) def _import_prowler_wrapper(self): """Helper to import prowler_wrapper with mocked WAZUH_PATH""" sys.path.insert(0, os.path.dirname(self.prowler_wrapper_path)) # Mock the WAZUH_PATH that's read at module level with patch("builtins.open", create=True) as mock_open: mock_open.return_value.readline.return_value = 'DIRECTORY="/opt/wazuh"' import importlib.util spec = importlib.util.spec_from_file_location( "prowler_wrapper", self.prowler_wrapper_path ) prowler_wrapper = importlib.util.module_from_spec(spec) spec.loader.exec_module(prowler_wrapper) return prowler_wrapper._run_prowler def test_command_injection_semicolon(self): """Test command injection using semicolon""" # Create a test file that should not be created if injection is prevented test_file = os.path.join(self.test_dir, "pwned.txt") # Malicious profile that attempts to create a file malicious_profile = f"test; touch {test_file}" # Mock the subprocess.Popen to capture the command with patch("subprocess.Popen") as mock_popen: mock_process = MagicMock() mock_process.communicate.return_value = (b"test output", None) mock_popen.return_value = mock_process # Import and run the vulnerable function _run_prowler = self._import_prowler_wrapper() # Run with malicious input _run_prowler(f'-p "{malicious_profile}" -V') # Check that Popen was called self.assertTrue(mock_popen.called) # Get the actual command that was passed to Popen actual_command = mock_popen.call_args[0][0] # With the fix, the command should be a list (from shlex.split) # and should NOT have shell=True self.assertIsInstance( actual_command, list, "Command should be a list after shlex.split" ) # Check that shell=True is not in the call call_kwargs = mock_popen.call_args[1] self.assertNotIn( "shell", call_kwargs, "shell parameter should not be present (defaults to False)", ) def test_command_injection_ampersand(self): """Test command injection using ampersand""" # Create a test file that should not be created if injection is prevented test_file = os.path.join(self.test_dir, "pwned2.txt") # Malicious profile that attempts to create a file malicious_profile = f"test && touch {test_file}" with patch("subprocess.Popen") as mock_popen: mock_process = MagicMock() mock_process.communicate.return_value = (b"test output", None) mock_popen.return_value = mock_process # Import and run the function _run_prowler = self._import_prowler_wrapper() # Run with malicious input _run_prowler(f'-p "{malicious_profile}" -V') # Get the actual command actual_command = mock_popen.call_args[0][0] # Verify it's a list (safe execution) self.assertIsInstance(actual_command, list) # The malicious characters should be preserved as part of the argument # not interpreted as shell commands command_str = " ".join(actual_command) self.assertIn( "&&", command_str, "Shell metacharacters should be preserved as literals", ) def test_command_injection_pipe(self): """Test command injection using pipe""" malicious_profile = 'test | echo "injected"' with patch("subprocess.Popen") as mock_popen: mock_process = MagicMock() mock_process.communicate.return_value = (b"test output", None) mock_popen.return_value = mock_process # Import and run the function _run_prowler = self._import_prowler_wrapper() # Run with malicious input _run_prowler(f'-p "{malicious_profile}" -V') # Get the actual command actual_command = mock_popen.call_args[0][0] # Verify safe execution self.assertIsInstance(actual_command, list) # Pipe should be preserved as literal command_str = " ".join(actual_command) self.assertIn("|", command_str) def test_command_injection_backticks(self): """Test command injection using backticks""" malicious_profile = "test `echo injected`" with patch("subprocess.Popen") as mock_popen: mock_process = MagicMock() mock_process.communicate.return_value = (b"test output", None) mock_popen.return_value = mock_process # Import and run the function _run_prowler = self._import_prowler_wrapper() # Run with malicious input _run_prowler(f'-p "{malicious_profile}" -V') # Get the actual command actual_command = mock_popen.call_args[0][0] # Verify safe execution self.assertIsInstance(actual_command, list) # Backticks should be preserved as literals command_str = " ".join(actual_command) self.assertIn("`", command_str) def test_command_injection_dollar_parentheses(self): """Test command injection using $() syntax""" malicious_profile = "test $(echo injected)" with patch("subprocess.Popen") as mock_popen: mock_process = MagicMock() mock_process.communicate.return_value = (b"test output", None) mock_popen.return_value = mock_process # Import and run the function _run_prowler = self._import_prowler_wrapper() # Run with malicious input _run_prowler(f'-p "{malicious_profile}" -V') # Get the actual command actual_command = mock_popen.call_args[0][0] # Verify safe execution self.assertIsInstance(actual_command, list) # $() should be preserved as literals command_str = " ".join(actual_command) self.assertIn("$(", command_str) def test_legitimate_profile_name(self): """Test that legitimate profile names still work correctly""" legitimate_profile = "production-aws-profile" with patch("subprocess.Popen") as mock_popen: mock_process = MagicMock() mock_process.communicate.return_value = (b"test output", None) mock_popen.return_value = mock_process # Import and run the function _run_prowler = self._import_prowler_wrapper() # Run with legitimate input result = _run_prowler(f"-p {legitimate_profile} -V") # Verify the function returns output self.assertEqual(result, b"test output") # Verify Popen was called correctly actual_command = mock_popen.call_args[0][0] self.assertIsInstance(actual_command, list) # Check the profile is passed correctly command_str = " ".join(actual_command) self.assertIn(legitimate_profile, command_str) def test_shlex_split_behavior(self): """Test that shlex properly handles quoted arguments""" profile_with_spaces = "my profile name" with patch("subprocess.Popen") as mock_popen: mock_process = MagicMock() mock_process.communicate.return_value = (b"test output", None) mock_popen.return_value = mock_process # Import and run the function _run_prowler = self._import_prowler_wrapper() # Run with profile containing spaces _run_prowler(f'-p "{profile_with_spaces}" -V') # Get the actual command actual_command = mock_popen.call_args[0][0] # Verify it's properly split self.assertIsInstance(actual_command, list) # The profile name should be preserved as a single argument # despite containing spaces self.assertIn("my profile name", actual_command) if __name__ == "__main__": unittest.main()