Tools · Python and Bash for security automation · 09 / 20
Running commands safely from Python: subprocess without shell injection
See how shell=True with user input enables command injection, rewrite calls with argument lists, validate inputs, and avoid the same mistake in os.system and similar APIs.
The problem
Security scripts often wrap tools such as ping, dig, or nmap. If a script builds a command string from user input and runs it through a shell, characters like ; | & $( ) let the input run extra commands. This is command injection, and it turns a helpful internal tool into a remote shell for whoever can reach it.
The fix
Pass an argument list to subprocess.run without shell=True. Each list item becomes exactly one argument, so shell metacharacters have no meaning. Validate inputs against what you expect, such as a hostname or IP pattern, and use -- to stop option parsing where tools support it.
import ipaddress, re, subprocess
# unsafe: input becomes shell syntax
# subprocess.run(f'ping -c 1 {host}', shell=True)
HOST = re.compile(r'^(?=.{1,253}$)[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*#x27;)
def valid_target(value: str) -> bool:
try:
ipaddress.ip_address(value); return True
except ValueError:
return bool(HOST.match(value))
def ping(host: str) -> str:
if not valid_target(host):
raise ValueError('not a hostname or IP')
r = subprocess.run(['ping', '-c', '1', '--', host], capture_output=True, text=True, timeout=10)
return r.stdoutLab: see it, then fix it
In a local lab script that is not reachable from the network, call the unsafe version with the input 127.0.0.1; id and observe that id runs. Then switch to the list form and see ping fail on the invalid argument instead. Add the validator and confirm the bad input is rejected before any process starts. Finally, run Bandit, a Python security linter, on the file and see it flag the shell=True call.
pip install bandit bandit -q wrapper.py
Diagram
shell=True with input versus argument list
The list form removes the shell from the picture.
Input. 127.0.0.1; id
Shell sees. ping -c 1 127.0.0.1 ; id
Result. Two commands run
Input. 127.0.0.1; id
Process gets. ['ping', '-c', '1', '--', '127.0.0.1; id']
Result. ping rejects one odd argument
Check yourself
1. What risk does subprocess.run(f"ping {host}", shell=True) create?
2. Which call is safe against shell injection?
3. Which tool flags risky subprocess usage in Python code?