I recently found a shell injection bug in some Ruby-gem I use.
Shell injections have scared me for a long time and I usually prefer to whitelist certain characters/patterns rather than to blacklist. This means that the system fails to the save side. Unfortunally it usually does fail – my whitelisting is to rigorous and data that would not cause any problems gets rejected. So I decided to take the opportunity to investigate how to prevent shell injection in my favorite scripting language (Python), the language I found the problem in and finally the language that I can not avoid (PHP).
Ruby
In Ruby you can use exec and system in a shell injection save way. If you pass a single string to them they will expand all shell characters, but if you pass the command and arguments separately, they won’t be interpreted by the shell:
irb(main):011:0> system("echo -e $(seq 5)") 1 2 3 4 5 => true irb(main):012:0> system("echo", "-e", "$(seq 5)") $(seq 5) => true
Unfortunally IO.popen and the backticks don’t have such a feature. Ruby really leaves you in the rain here (and that triggers bugs like the one mentioned in the intro).
UPDATE: As 13k mentioned below there is a stdlib method now: shellescape.
Here the escape-library jumps in:
irb(main):001:0> require 'escape' => true irb(main):002:0> s=`#{Escape.shell_command(["echo", "$(seq)", "\"'`seq` && ||"])}` => "$(seq) \"'`seq` && ||\n"
Something like that should really become part of the ruby standard library.
Python
Since version 2.4 Python has the subprocess-module which claims to handle arguments securly:
Unlike some other popen functions, this implementation will never call /bin/sh implicitly. This means that all characters, including shell metacharacters, can safely be passed to child processes.
By default shell characters will not be expanded.
>>> import subprocess >>> subprocess.call(["ls", "*.c"]) ls: *.c: No such file or directory
However, you can tell it, to do so by setting shell=True
:
>>> import subprocess >>> subprocess.call("ls *.c",shell=True) crack_rsa_in_qudratic_time.c
Did you notice, that the syntax slightly changed? I passed "ls *.c"
instead of ["ls","*.c"]
.
The latter wont work without shell=True
:
>>> subprocess.call(["ls", "*.c"],shell=True) a_short_proof_of_fermats_last_theorem.tex loveletter.tex crack_rsa_in_qudratic_time.c showargs.sh 0
In fact it just calls “ls”. It took me quite some time to figure out, what’s going on.
Digging through the subprocess source I found:
if shell: args = ["/bin/sh", "-c"] + args if executable is None: executable = args[0] .... if env is None: os.execvp(executable, args) else: os.execvpe(executable, args, env)
So calling subprocess.call(["ls", "*.c"],shell=True)
is (roughly) equivalent to os.execvp("/bin/sh", ["-c", "ls", "-l")
.
And the man page of bash explains:
-c string If the -c option is present, then commands are read from string. If there are arguments after the string, they are assigned to the positional parameters, starting with $0.
Reading this carfully you might guess that sh -c
in fact does not “call” the program after the -c
parameter but invokes the “shell-script” and sets the positional parameters $0, $1, $2, ...
according to the following arguments. The following should clarify what’s going on:
>>> import subprocess >>> subprocess.call(['echo $0 $1 $2 $3', 'arg0', 'arg1', 'arg2', 'arg3'], shell=True) arg0 arg1 arg2 arg3 0
This is a bit sad, since it implies that using shell=True
, changes the syntax of of call()
and Popen()
. If you really want to use shell expansion, it is probably best to put the whole command in one string.
Read more about subprocess.
PHP
PHP offers the usual commands:
system(), exec(), passthru() and backticks. None of them protects you from shell injection by default. You have to use escapeshellarg() or escapeshellcmd().