Python Subprocess with Tainted Environment Arguments

High Risk Command Injection
PythonsubprocessEnvironment VariablesCommand InjectionTainted InputSecurity Audit

What it is

Application uses subprocess with environment variables or arguments derived from user input, potentially allowing command injection or environment manipulation attacks.

import subprocess import os from flask import request @app.route('/run_with_env') def run_with_env(): # Vulnerable: User input directly in environment user_var = request.args.get('env_var') env = os.environ.copy() env['USER_INPUT'] = user_var # Tainted environment variable result = subprocess.run( ['printenv', 'USER_INPUT'], env=env, capture_output=True, text=True ) return result.stdout @app.route('/execute_script') def execute_script(): # Vulnerable: Tainted arguments from user script_args = request.args.getlist('args') cmd = ['python', 'script.py'] + script_args # Unsafe concatenation result = subprocess.run(cmd, capture_output=True, text=True) return result.stdout @app.route('/custom_path') def custom_path(): # Vulnerable: User controls PATH environment custom_path = request.args.get('path') env = {'PATH': custom_path} # Dangerous PATH manipulation result = subprocess.run( ['ls'], env=env, capture_output=True, text=True ) return result.stdout
import subprocess import re from flask import request # Allowlists for safe values ALLOWED_ENV_VARS = ['LANG', 'LC_ALL', 'TZ'] ALLOWED_ARG_PATTERN = re.compile(r'^[a-zA-Z0-9_\-\.]+$') @app.route('/run_with_env') def run_with_env(): # Secure: Validate and sanitize environment variables user_var = request.args.get('env_var', '') # Validate environment variable value if not user_var or not re.match(r'^[a-zA-Z0-9_\-\.]+$', user_var): return 'Invalid environment variable value', 400 # Use explicit safe environment safe_env = { 'PATH': '/usr/bin:/bin', # Fixed safe PATH 'USER_INPUT': user_var[:50] # Limit length } result = subprocess.run( ['printenv', 'USER_INPUT'], env=safe_env, capture_output=True, text=True, timeout=5 ) return result.stdout @app.route('/execute_script') def execute_script(): # Secure: Validate all arguments script_args = request.args.getlist('args') # Validate each argument validated_args = [] for arg in script_args[:5]: # Limit number of args if ALLOWED_ARG_PATTERN.match(arg) and len(arg) <= 50: validated_args.append(arg) else: return f'Invalid argument: {arg}', 400 # Safe command construction cmd = ['python3', '/opt/scripts/safe_script.py'] + validated_args # Safe environment safe_env = { 'PATH': '/usr/bin:/bin', 'PYTHONPATH': '/opt/scripts' } try: result = subprocess.run( cmd, env=safe_env, capture_output=True, text=True, timeout=30 ) return result.stdout except subprocess.TimeoutExpired: return 'Script execution timeout', 408 @app.route('/custom_path') def custom_path(): # Secure: Use fixed safe environment # Don't allow user to control PATH safe_env = { 'PATH': '/usr/bin:/bin:/usr/local/bin', 'HOME': '/tmp', 'USER': 'www-data' } result = subprocess.run( ['ls', '/tmp'], # Fixed safe directory env=safe_env, capture_output=True, text=True, timeout=5 ) return result.stdout # Secure helper function for environment validation def create_safe_environment(base_vars=None): """Create a safe environment with only allowed variables.""" safe_env = { 'PATH': '/usr/bin:/bin', 'HOME': '/tmp', 'LANG': 'en_US.UTF-8' } if base_vars: for key, value in base_vars.items(): if key in ALLOWED_ENV_VARS and isinstance(value, str): # Sanitize value clean_value = re.sub(r'[^a-zA-Z0-9_\-\./:]', '', value) safe_env[key] = clean_value[:100] # Limit length return safe_env

💡 Why This Fix Works

See fix suggestions for detailed explanation.

Why it happens

Code sets environment from user input: subprocess.run(cmd, env={'VAR': request.args['value']}). Attackers control environment variables affecting command behavior. Variables like PATH, LD_PRELOAD, PYTHONPATH enable code execution. User-controlled env parameters create injection vectors through environment manipulation.

Root causes

Passing User-Controlled Data in subprocess env Parameter

Code sets environment from user input: subprocess.run(cmd, env={'VAR': request.args['value']}). Attackers control environment variables affecting command behavior. Variables like PATH, LD_PRELOAD, PYTHONPATH enable code execution. User-controlled env parameters create injection vectors through environment manipulation.

Using os.environ with User Input to Set Environment Variables

Modifying global environment with user data: os.environ['CONFIG_PATH'] = user_path; subprocess.run(command). Changes affect all subprocess calls. Persistent across multiple operations. Race conditions if concurrent requests. User input in environment enables configuration manipulation and code execution.

Not Sanitizing Environment Variables Before Subprocess Calls

Inheriting full parent environment: subprocess.run(cmd, env=os.environ). If os.environ contains user-controlled values from earlier operations, subprocess inherits them. No validation or sanitization. Tainted environment variables from web requests propagate to subprocess, creating delayed injection vulnerabilities.

Allowing User Control of PATH or Library Loading Variables

User input affects PATH: env = {'PATH': f'/custom/{user_dir}:/usr/bin'}. Attackers inject malicious directories into PATH. Commands load attacker binaries. LD_LIBRARY_PATH, LD_PRELOAD, PYTHONPATH similarly dangerous. Library loading variables enable arbitrary code execution before command runs.

Using subprocess with Environment Variables from External Sources

Environment from databases or config files without validation: env_vars = json.loads(config); subprocess.run(cmd, env=env_vars). If external source compromised or contains attacker-influenced data, subprocess executes with malicious environment. Trust boundary violations when environment source not validated.

Fixes

1

Never Include User Input in subprocess env Parameter

Use hardcoded environment only: env = {'PATH': '/usr/bin:/bin', 'HOME': '/tmp', 'LANG': 'en_US.UTF-8'}. No user data in environment variables. Build allowlist of required variables with static values. Pass user data through command arguments (validated), never environment.

2

Provide Explicit Minimal Environment Dictionary

Set minimal env explicitly: subprocess.run(cmd, env={'PATH': '/usr/bin'}, shell=False). Don't use env=os.environ. Only include variables command requires. Empty dict env={} provides maximum isolation. Add variables individually with static values. Explicit environment prevents inheriting tainted variables.

3

Validate Environment Variable Values Against Allowlists

If environment must be dynamic, validate strictly: ALLOWED_LOCALES = {'en_US.UTF-8', 'fr_FR.UTF-8'}; if locale in ALLOWED_LOCALES: env['LANG'] = locale. Allowlist approach for values. Validate format: re.match(r'^[A-Z_]+=[\w./:-]+$'). Reject unexpected patterns. Validation prevents environment injection.

4

Use Subprocess Arguments Instead of Environment Variables

Pass configuration through command arguments: subprocess.run(['tool', '--config', config_file], env=minimal_env). Arguments easier to validate than environment. Explicit and visible. Less likely to have hidden side effects. Use arguments for all user-controlled configuration, reserve environment for static system settings.

5

Never Modify os.environ, Always Use Local env Parameter

Avoid os.environ['KEY'] = value before subprocess. Use local env dict: my_env = {'KEY': 'value'}; subprocess.run(cmd, env=my_env). Prevents global state pollution. No race conditions. Changes isolated to single call. os.environ modifications affect entire process, creating difficult-to-debug vulnerabilities.

6

Sanitize and Validate External Environment Configuration

If loading environment from config, validate each entry: for key, value in config_env.items(): if not is_safe_env_var(key, value): raise ValueError(). Check key names match pattern. Validate value format. Reject dangerous variables like PATH, LD_*. Use schema validation for environment configuration.

Detect This Vulnerability in Your Code

Sourcery automatically identifies python subprocess with tainted environment arguments and many other security issues in your codebase.