Safe Argument Arrays and Injection Mitigation

🏷️ Operating System and System Operations / Shell Commands with Subprocess

When running shell commands from Python, one of the most common mistakes is passing user input directly into a command string. This opens the door to command injection attacks, where a malicious user can execute arbitrary commands on your system. Understanding how to safely pass arguments is essential for writing secure automation scripts.


🧠 Why Command Injection Happens

  • When you build a command as a single string, Python sends that entire string to the shell for parsing
  • The shell interprets special characters like semicolons, pipes, and dollar signs as command separators or variables
  • If user input contains something like ; rm -rf /, the shell may execute that as a separate command
  • This is especially dangerous when running scripts that accept filenames, IP addresses, or other external data

⚙️ The Problem with String-Based Commands

  • Using subprocess.run("ping " + user_input, shell=True) passes the entire string to the shell
  • The shell sees the full string and interprets any special characters
  • Example: If user_input is 8.8.8.8; whoami, the shell runs both ping 8.8.8.8 and whoami
  • This is the root cause of most command injection vulnerabilities in Python scripts

🛠️ Safe Approach: Argument Arrays

  • Instead of a single string, pass a list of arguments to subprocess.run()
  • Each element in the list becomes a separate argument to the command
  • Python handles escaping and quoting automatically, so no shell interpretation occurs
  • Example: subprocess.run(["ping", "-c", "4", user_input]) treats user_input as a single argument, even if it contains spaces or special characters
  • The command is executed directly without going through a shell interpreter

📊 Comparison: String vs Array Approach

Aspect String with shell=True Array without shell=True
Shell interpretation Yes, full parsing No, direct execution
Injection risk High None
Special characters Interpreted as commands Treated as literal text
Spaces in arguments Breaks arguments Preserved as single argument
Performance Slower (spawns shell) Faster (direct exec)
Recommended for user input Never Always

🕵️ When to Avoid shell=True Entirely

  • If your command does not need shell features like pipes, redirects, or environment variable expansion, always set shell=False (which is the default)
  • For simple commands like ping, grep, ls, or curl, use argument arrays
  • If you absolutely need shell features, consider using shlex.split() to safely parse a command string into an array
  • Example: subprocess.run(shlex.split("ping -c 4 8.8.8.8")) converts the string to a safe array before execution

🔐 Additional Mitigation Techniques

  • Validate and sanitize all user input before passing it to any subprocess call
  • Use whitelisting for allowed characters or values when possible
  • Avoid constructing command strings with string concatenation or f-strings
  • For complex commands, consider using Python libraries that wrap the command safely (like sh or plumbum)
  • Always set shell=False unless you have a specific, justified reason to enable it

📋 Best Practices Summary

  • Always use argument arrays instead of command strings
  • Never set shell=True when accepting user input
  • Treat all external input as untrusted and potentially malicious
  • Use shlex.split() if you must work with command strings from configuration files
  • Test your scripts with inputs containing spaces, semicolons, and quotes to verify safety
  • Prefer Python-native solutions over shell commands whenever possible

By adopting argument arrays and avoiding shell interpretation, you eliminate the most common vector for command injection attacks. This simple shift in how you call subprocesses makes your scripts significantly more secure and reliable in production environments.


Safe argument arrays prevent shell injection by passing commands as lists instead of strings, so arguments are never interpreted by the shell.


🛡️ Example 1: Unsafe string command that can be exploited

This shows how a string command allows shell injection — a malicious user could run extra commands.

import subprocess

filename = "file.txt; rm -rf /"
command = f"ls -l {filename}"
result = subprocess.run(command, shell=True, capture_output=True, text=True)
print(result.stdout)
print(result.stderr)

📤 Output: (empty output or error — the injected command may execute)


✅ Example 2: Safe argument array prevents injection

This passes arguments as a list, so the filename is treated as a literal argument, not a shell command.

import subprocess

filename = "file.txt; rm -rf /"
result = subprocess.run(["ls", "-l", filename], capture_output=True, text=True)
print(result.stdout)
print(result.stderr)

📤 Output: ls: cannot access 'file.txt; rm -rf /': No such file or directory


📁 Example 3: Listing files with a safe argument array

This demonstrates a normal file listing using a list of arguments — no shell involved.

import subprocess

result = subprocess.run(["ls", "-la", "/home/engineer"], capture_output=True, text=True)
print(result.stdout)

📤 Output: total 32\ndrwxr-xr-x 2 engineer engineer 4096 ...


🔍 Example 4: Grepping with user input safely

This shows how to safely pass user-provided search terms without risk of injection.

import subprocess

search_term = "error"
result = subprocess.run(["grep", "-i", search_term, "/var/log/syslog"], capture_output=True, text=True)
print(result.stdout[:200])

📤 Output: Jan 15 10:23:45 server kernel: [ 1234.567890] error: ...


📄 Example 5: Copying files with dynamic paths

This demonstrates safe file operations when paths come from variables or user input.

import subprocess

source = "/home/engineer/report.txt"
destination = "/tmp/backup/report.txt"
result = subprocess.run(["cp", source, destination], capture_output=True, text=True)
print(result.returncode)

📤 Output: 0


📊 Comparison: Unsafe vs Safe Argument Passing

Approach Example Risk Level
String with shell=True subprocess.run("ls " + filename, shell=True) High — shell injection possible
Argument list subprocess.run(["ls", filename]) Low — arguments are literal
String without shell=True subprocess.run("ls " + filename) Error — requires shell=True for strings

When running shell commands from Python, one of the most common mistakes is passing user input directly into a command string. This opens the door to command injection attacks, where a malicious user can execute arbitrary commands on your system. Understanding how to safely pass arguments is essential for writing secure automation scripts.


🧠 Why Command Injection Happens

  • When you build a command as a single string, Python sends that entire string to the shell for parsing
  • The shell interprets special characters like semicolons, pipes, and dollar signs as command separators or variables
  • If user input contains something like ; rm -rf /, the shell may execute that as a separate command
  • This is especially dangerous when running scripts that accept filenames, IP addresses, or other external data

⚙️ The Problem with String-Based Commands

  • Using subprocess.run("ping " + user_input, shell=True) passes the entire string to the shell
  • The shell sees the full string and interprets any special characters
  • Example: If user_input is 8.8.8.8; whoami, the shell runs both ping 8.8.8.8 and whoami
  • This is the root cause of most command injection vulnerabilities in Python scripts

🛠️ Safe Approach: Argument Arrays

  • Instead of a single string, pass a list of arguments to subprocess.run()
  • Each element in the list becomes a separate argument to the command
  • Python handles escaping and quoting automatically, so no shell interpretation occurs
  • Example: subprocess.run(["ping", "-c", "4", user_input]) treats user_input as a single argument, even if it contains spaces or special characters
  • The command is executed directly without going through a shell interpreter

📊 Comparison: String vs Array Approach

Aspect String with shell=True Array without shell=True
Shell interpretation Yes, full parsing No, direct execution
Injection risk High None
Special characters Interpreted as commands Treated as literal text
Spaces in arguments Breaks arguments Preserved as single argument
Performance Slower (spawns shell) Faster (direct exec)
Recommended for user input Never Always

🕵️ When to Avoid shell=True Entirely

  • If your command does not need shell features like pipes, redirects, or environment variable expansion, always set shell=False (which is the default)
  • For simple commands like ping, grep, ls, or curl, use argument arrays
  • If you absolutely need shell features, consider using shlex.split() to safely parse a command string into an array
  • Example: subprocess.run(shlex.split("ping -c 4 8.8.8.8")) converts the string to a safe array before execution

🔐 Additional Mitigation Techniques

  • Validate and sanitize all user input before passing it to any subprocess call
  • Use whitelisting for allowed characters or values when possible
  • Avoid constructing command strings with string concatenation or f-strings
  • For complex commands, consider using Python libraries that wrap the command safely (like sh or plumbum)
  • Always set shell=False unless you have a specific, justified reason to enable it

📋 Best Practices Summary

  • Always use argument arrays instead of command strings
  • Never set shell=True when accepting user input
  • Treat all external input as untrusted and potentially malicious
  • Use shlex.split() if you must work with command strings from configuration files
  • Test your scripts with inputs containing spaces, semicolons, and quotes to verify safety
  • Prefer Python-native solutions over shell commands whenever possible

By adopting argument arrays and avoiding shell interpretation, you eliminate the most common vector for command injection attacks. This simple shift in how you call subprocesses makes your scripts significantly more secure and reliable in production environments.

Interactive Views

You are currently in 📚 All-in-One mode. Use the tabs at the top to switch to 📖 Theory Only or 💻 Code Only views.

Safe argument arrays prevent shell injection by passing commands as lists instead of strings, so arguments are never interpreted by the shell.


🛡️ Example 1: Unsafe string command that can be exploited

This shows how a string command allows shell injection — a malicious user could run extra commands.

import subprocess

filename = "file.txt; rm -rf /"
command = f"ls -l {filename}"
result = subprocess.run(command, shell=True, capture_output=True, text=True)
print(result.stdout)
print(result.stderr)

📤 Output: (empty output or error — the injected command may execute)


✅ Example 2: Safe argument array prevents injection

This passes arguments as a list, so the filename is treated as a literal argument, not a shell command.

import subprocess

filename = "file.txt; rm -rf /"
result = subprocess.run(["ls", "-l", filename], capture_output=True, text=True)
print(result.stdout)
print(result.stderr)

📤 Output: ls: cannot access 'file.txt; rm -rf /': No such file or directory


📁 Example 3: Listing files with a safe argument array

This demonstrates a normal file listing using a list of arguments — no shell involved.

import subprocess

result = subprocess.run(["ls", "-la", "/home/engineer"], capture_output=True, text=True)
print(result.stdout)

📤 Output: total 32\ndrwxr-xr-x 2 engineer engineer 4096 ...


🔍 Example 4: Grepping with user input safely

This shows how to safely pass user-provided search terms without risk of injection.

import subprocess

search_term = "error"
result = subprocess.run(["grep", "-i", search_term, "/var/log/syslog"], capture_output=True, text=True)
print(result.stdout[:200])

📤 Output: Jan 15 10:23:45 server kernel: [ 1234.567890] error: ...


📄 Example 5: Copying files with dynamic paths

This demonstrates safe file operations when paths come from variables or user input.

import subprocess

source = "/home/engineer/report.txt"
destination = "/tmp/backup/report.txt"
result = subprocess.run(["cp", source, destination], capture_output=True, text=True)
print(result.returncode)

📤 Output: 0


📊 Comparison: Unsafe vs Safe Argument Passing

Approach Example Risk Level
String with shell=True subprocess.run("ls " + filename, shell=True) High — shell injection possible
Argument list subprocess.run(["ls", filename]) Low — arguments are literal
String without shell=True subprocess.run("ls " + filename) Error — requires shell=True for strings