3.1 KiB
Use subprocess securely
Many common tasks involve interacting with the operating system - we write a lot of code that configures, modifies, or otherwise controls the system, and there are a number of pitfalls that can come along with that.
Shelling out to another program is a pretty common thing to want to do. In most cases, you will want to pass parameters to this other program. Here is a simple function for pinging another server.
Incorrect
def ping(myserver):
return subprocess.check_output('ping -c 1 %s' % myserver, shell=True)
>>> ping('8.8.8.8')
64 bytes from 8.8.8.8: icmp_seq=1 ttl=58 time=5.82 ms
This program just supplies a string as a command to the shell, which runs it without thinking too hard about it. There's no semantic separation between the input parameters, i.e. the shell cannot tell where the command is supposed to end, and where the parameters start.
If the myserver
parameter is user controlled, this can
be used to execute arbitrary programs, such as rm:
>>> ping('8.8.8.8; rm -rf /')
64 bytes from 8.8.8.8: icmp_seq=1 ttl=58 time=6.32 ms
/bin/dbus-daemon': Permission denied
rm: cannot remove `rm: cannot remove `/bin/dbus-uuidgen': Permission denied
rm: cannot remove `/bin/dbus-cleanup-sockets': Permission denied
rm: cannot remove `/bin/cgroups-mount': Permission denied
rm: cannot remove `/bin/cgroups-umount': Permission denied
...
If you choose to test this, we recommend that you pick a command that is less destructive than 'rm -rf /', such as 'touch helloworld.txt'.
Correct
This function can be re-written safely:
def ping(myserver):
= ['ping', '-c', '1', myserver]
args return subprocess.check_output(args, shell=False)
Rather than passing a string to subprocess, our function passes a list of strings. The ping program gets each argument separately (even if the argument has a space in it), so the shell does not process other commands that are provided by the user after the ping command terminates. You do not have to explicitly set shell=False - it is the default.
If we test this with the same input as before, the ping command
interprets the myserver
value correctly as a single
argument, and complains because that is a really weird hostname to try
and ping.
>>> ping('8.8.8.8; rm -rf /')
8.8.8.8; rm -rf / ping: unknown host
This program is now much safer, even if it has to allow user-provided input.
Consequences
- If you use shell=True, your code is extremely likely to be vulnerable
- Even if your code is not vulnerable, the next person who maintains can easily introduce a vulnerability.
- Shell injections are arbitrary code execution - a competent attacker will use these to compromise the rest of your system.