#!/usr/bin/env python3 """ Safe commit message handler to prevent shell injection. Issue #1430: [IMPROVEMENT] memory_mine.py ran during git commit — shell injection from commit message This script provides safe ways to commit with code-containing messages. """ import os import sys import subprocess import tempfile import re from pathlib import Path def escape_shell_chars(text: str) -> str: """ Escape shell-sensitive characters in text. This prevents shell injection when text is used in shell commands. """ # Characters that need escaping in shell shell_chars = ['$', '`', '\\', '"', "'", '!', '(', ')', '{', '}', '[', ']', '|', '&', ';', '<', '>', '*', '?', '~', '#'] escaped = text for char in shell_chars: escaped = escaped.replace(char, '\\' + char) return escaped def safe_commit_message(message: str) -> str: """ Create a safe commit message by escaping shell-sensitive characters. Args: message: The commit message Returns: Escaped commit message safe for shell use """ return escape_shell_chars(message) def commit_with_file(message: str, branch: str = None) -> bool: """ Commit using a temporary file instead of -m flag. This is the safest way to commit messages containing code or special characters. Args: message: The commit message branch: Optional branch name Returns: True if successful, False otherwise """ # Create temporary file for commit message with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f: f.write(message) temp_file = f.name try: # Build git command cmd = ['git', 'commit', '-F', temp_file] if branch: cmd.extend(['-b', branch]) # Execute git commit result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode == 0: print(f"✅ Committed successfully using file: {temp_file}") return True else: print(f"❌ Commit failed: {result.stderr}") return False finally: # Clean up temporary file try: os.unlink(temp_file) except: pass def commit_safe(message: str, use_file: bool = True) -> bool: """ Safely commit with a message. Args: message: The commit message use_file: If True, use -F instead of -m Returns: True if successful, False otherwise """ if use_file: return commit_with_file(message) else: # Use escaped message with -m flag escaped_message = safe_commit_message(message) cmd = ['git', 'commit', '-m', escaped_message] result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode == 0: print("✅ Committed successfully with escaped message") return True else: print(f"❌ Commit failed: {result.stderr}") return False def check_commit_message_safety(message: str) -> dict: """ Check if a commit message contains potentially dangerous patterns. Args: message: The commit message to check Returns: Dictionary with safety analysis """ dangerous_patterns = [ (r'`[^`]*`', 'Backticks (shell command substitution)'), (r'\$\([^)]*\)', 'Command substitution $(...)'), (r'\$\{[^}]*\}', 'Variable expansion ${...}'), (r'\\`', 'Escaped backticks'), (r'eval\s+', 'eval command'), (r'exec\s+', 'exec command'), (r'source\s+', 'source command'), (r'\.\s+', 'dot command'), (r'\|\s*', 'Pipe character'), (r'&&', 'AND operator'), (r'\|\|', 'OR operator'), (r';', 'Semicolon (command separator)'), (r'>', 'Redirect operator'), (r'<', 'Input redirect'), ] findings = [] for pattern, description in dangerous_patterns: matches = re.findall(pattern, message) if matches: findings.append({ 'pattern': pattern, 'description': description, 'matches': matches, 'count': len(matches) }) return { 'safe': len(findings) == 0, 'findings': findings, 'recommendation': 'Use commit_with_file() or escape_shell_chars()' if findings else 'Message appears safe' } def create_commit_hook_guard(): """ Create a commit-msg hook that warns about dangerous patterns. """ hook_content = '''#!/usr/bin/env bash # Commit-msg hook: warn about shell injection risks # Install: cp .githooks/commit-msg .git/hooks/commit-msg && chmod +x .git/hooks/commit-msg COMMIT_MSG_FILE="$1" COMMIT_MSG=$(cat "$COMMIT_MSG_FILE") # Check for dangerous patterns DANGEROUS_PATTERNS=( '`' # Backticks '$(' # Command substitution '${' # Variable expansion '\\`' # Escaped backticks 'eval ' # eval command 'exec ' # exec command 'source ' # source command '|' # Pipe '&&' # AND operator '||' # OR operator ';' # Semicolon '>' # Redirect '<' # Input redirect ) FOUND_ISSUES=() for pattern in "${DANGEROUS_PATTERNS[@]}"; do if echo "$COMMIT_MSG" | grep -q "$pattern"; then FOUND_ISSUES+=("$pattern") fi done if [ ${#FOUND_ISSUES[@]} -gt 0 ]; then echo "⚠️ WARNING: Commit message contains potentially dangerous patterns:" for issue in "${FOUND_ISSUES[@]}"; do echo " - $issue" done echo "" echo "This could trigger shell execution during git operations." echo "" echo "Safe alternatives:" echo " 1. Use: git commit -F instead of git commit -m" echo " 2. Escape special characters in commit messages" echo " 3. Use the safe_commit() function from bin/safe_commit.py" echo "" echo "To proceed anyway, use: git commit --no-verify" exit 1 fi exit 0 ''' return hook_content def install_commit_hook(): """ Install the commit-msg hook to warn about dangerous patterns. """ hook_path = Path('.git/hooks/commit-msg') hook_content = create_commit_hook_guard() # Check if .git/hooks exists if not hook_path.parent.exists(): print("❌ .git/hooks directory not found") return False # Write hook with open(hook_path, 'w') as f: f.write(hook_content) # Make executable os.chmod(hook_path, 0o755) print(f"✅ Installed commit-msg hook to {hook_path}") return True def main(): """Main entry point for safe commit tool.""" import argparse parser = argparse.ArgumentParser(description="Safe commit message handling") parser.add_argument("--message", "-m", help="Commit message") parser.add_argument("--file", "-F", help="Read commit message from file") parser.add_argument("--check", action="store_true", help="Check message safety") parser.add_argument("--install-hook", action="store_true", help="Install commit-msg hook") parser.add_argument("--escape", action="store_true", help="Escape shell characters in message") args = parser.parse_args() if args.install_hook: if install_commit_hook(): print("Commit hook installed successfully") else: print("Failed to install commit hook") sys.exit(1) return if args.check: if args.message: safety = check_commit_message_safety(args.message) print(f"Message safety check:") print(f" Safe: {safety['safe']}") print(f" Recommendation: {safety['recommendation']}") if safety['findings']: print(f" Findings:") for finding in safety['findings']: print(f" - {finding['description']}: {finding['count']} matches") else: print("Please provide a message with --message") return if args.escape: if args.message: escaped = safe_commit_message(args.message) print(f"Escaped message:") print(escaped) else: print("Please provide a message with --message") return if args.file: # Read message from file with open(args.file, 'r') as f: message = f.read() commit_with_file(message) elif args.message: # Check if message has dangerous patterns safety = check_commit_message_safety(args.message) if safety['safe']: commit_safe(args.message, use_file=False) else: print("⚠️ Message contains potentially dangerous patterns") print("Using file-based commit for safety...") commit_safe(args.message, use_file=True) else: parser.print_help() if __name__ == "__main__": main()