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` && ||

"

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().