Python Dangerous Subprocess Usage Audit

High Risk Command Injection
PythonsubprocessCommand InjectionCode ExecutionSecurity AuditInput Validation

What it is

Application uses subprocess module in potentially dangerous ways that could lead to command injection vulnerabilities, especially when user input is involved in command construction.

import subprocess from flask import request @app.route('/execute') def execute_command(): # Vulnerable: User input directly in shell command cmd = request.args.get('cmd') result = subprocess.call(f'ls {cmd}', shell=True) return f'Command executed: {result}' @app.route('/ping') def ping_host(): # Vulnerable: Command injection through hostname host = request.args.get('host') command = f'ping -c 1 {host}' output = subprocess.check_output(command, shell=True) return output.decode() @app.route('/backup') def backup_file(): # Vulnerable: File path not validated filename = request.args.get('file') subprocess.run(['tar', '-czf', '/tmp/backup.tar.gz', filename]) return 'Backup created' # Vulnerable: Direct user input to subprocess def run_user_script(script_name): return subprocess.run(f'python {script_name}', shell=True)
import subprocess import shlex import os from flask import request # Safe command allowlist ALLOWED_COMMANDS = ['ls', 'pwd', 'date'] ALLOWED_PING_HOSTS = ['google.com', 'github.com'] @app.route('/execute') def execute_command(): # Secure: Validate against allowlist cmd = request.args.get('cmd') if cmd not in ALLOWED_COMMANDS: return 'Command not allowed', 400 # Secure: Use argument list, no shell try: result = subprocess.run([cmd], capture_output=True, text=True, timeout=5) return f'Output: {result.stdout}' except subprocess.TimeoutExpired: return 'Command timeout', 408 @app.route('/ping') def ping_host(): # Secure: Validate against allowlist host = request.args.get('host') if host not in ALLOWED_PING_HOSTS: return 'Host not allowed', 400 # Secure: Use argument list with validated input try: result = subprocess.run( ['ping', '-c', '1', host], capture_output=True, text=True, timeout=10 ) return result.stdout except subprocess.TimeoutExpired: return 'Ping timeout', 408 @app.route('/backup') def backup_file(): # Secure: Validate file path filename = request.args.get('file') # Validate filename if not filename or '..' in filename or filename.startswith('/'): return 'Invalid filename', 400 # Ensure file exists and is in allowed directory safe_path = os.path.join('/var/www/uploads', filename) if not os.path.exists(safe_path): return 'File not found', 404 # Secure: Use argument list with validated paths try: subprocess.run([ 'tar', '-czf', '/tmp/backup.tar.gz', safe_path ], timeout=30, check=True) return 'Backup created' except subprocess.CalledProcessError: return 'Backup failed', 500 # Secure: Properly validate and execute scripts def run_user_script(script_name): # Validate script name if not script_name.endswith('.py') or '..' in script_name: raise ValueError('Invalid script name') script_path = os.path.join('/var/scripts', script_name) if not os.path.exists(script_path): raise FileNotFoundError('Script not found') # Secure execution with timeout return subprocess.run( ['python3', script_path], capture_output=True, text=True, timeout=30 )

💡 Why This Fix Works

See fix suggestions for detailed explanation.

Why it happens

Developers call subprocess.run(), subprocess.call(), or subprocess.Popen() without security awareness. Mixing shell=True with user input creates injection risks. Passing string commands instead of lists. Not setting timeouts. subprocess power requires careful usage understanding to avoid vulnerabilities.

Root causes

Using subprocess Functions Without Understanding Security Implications

Developers call subprocess.run(), subprocess.call(), or subprocess.Popen() without security awareness. Mixing shell=True with user input creates injection risks. Passing string commands instead of lists. Not setting timeouts. subprocess power requires careful usage understanding to avoid vulnerabilities.

Passing User Input to subprocess with shell=True Parameter

Code executes commands with shell: subprocess.run(f'ping {host}', shell=True). Shell interprets metacharacters (; && | ` $()), enabling command injection. Input like host=example.com; rm -rf / executes arbitrary commands. shell=True should never be combined with user-controlled data.

Using subprocess Without Timeout or Resource Limits

Calling subprocess without timeout: subprocess.run(['long_command']). Command may hang indefinitely. No CPU or memory limits. Attackers trigger resource exhaustion through slow commands or infinite loops. Missing timeout enables denial of service. Resource limits prevent system resource exhaustion.

Not Validating Command Arguments or File Paths

Passing unvalidated arguments: subprocess.run(['tool', user_arg]). Even without shell=True, argument injection possible. Arguments like --config=/etc/passwd or -o ProxyCommand=malicious change command behavior. File paths with ../ enable path traversal. Validation required even with list syntax.

Using subprocess with Untrusted Environment Variables

Inheriting parent environment: subprocess.run(command, env=os.environ). User-controlled environment variables affect command behavior. PATH manipulation loads malicious binaries. LD_PRELOAD enables code injection. PYTHONPATH for Python subprocess. Environment should be explicitly controlled, not inherited.

Fixes

1

Never Use shell=True, Always Use List Syntax for Commands

Pass commands as lists: subprocess.run(['ping', '-c', '4', host], shell=False, check=True). Each list element is separate argument. No shell interpretation of metacharacters. shell=False prevents injection. Use check=True for automatic error handling. List syntax primary protection.

2

Always Set Timeout Parameter to Prevent Hanging

Specify timeout for all subprocess calls: subprocess.run(cmd, timeout=10, check=True). Raises TimeoutExpired if exceeded. Prevents indefinite hangs. Set reasonable limits based on expected duration. Use try/except to handle timeouts gracefully. Critical for user-facing operations.

3

Validate All Arguments Against Allowlists and Format Requirements

Validate before subprocess: ALLOWED_ARGS = {'-v', '--verbose'}; if arg not in ALLOWED_ARGS: abort(400). For file paths, use Path.resolve() and check is_relative_to(). Validate format: re.match(r'^[a-z0-9-]+$', option). Reject unexpected arguments. Allowlist validation prevents argument injection.

4

Explicitly Set Clean Environment Variables

Provide minimal env dict: subprocess.run(cmd, env={'PATH': '/usr/bin:/bin', 'HOME': '/tmp'}, shell=False). Don't inherit os.environ. Include only required variables. Prevents environment-based attacks. Use env={} for maximum isolation. Set HOME, USER explicitly if needed.

5

Use Python Libraries Instead of Subprocess Where Possible

Replace subprocess with native libraries: requests instead of curl, zipfile instead of tar, Pillow instead of convert, gitpython instead of git commands. Libraries provide safe APIs. Eliminate command injection surface. Better error handling and cross-platform compatibility.

6

Implement Least Privilege and Sandboxing for Subprocess Execution

Run with reduced privileges: subprocess.run(cmd, user='nobody'). Use containers, chroot, or seccomp for isolation. Drop capabilities. Set resource limits with rlimit. Even with injection, privilege separation limits damage. Defense-in-depth approach essential for subprocess security.

Detect This Vulnerability in Your Code

Sourcery automatically identifies python dangerous subprocess usage audit and many other security issues in your codebase.