Stop using backtick to run shell command in Ruby

It’s all too common to find the use of backtick to run shell command from Ruby. It’s fine when you just run a command that doesn’t take any user input. But when you start passing input from untrusted source, that’s when the trouble begins.

output = `echo #{user_input}`

The problem is that this is vulnerable to command injection. Consider this user input.

user_input = "hello; rm -rf *"
output = `echo #{user_input}`

This will echo ‘hello’, and then remove all files from the current directory. Not so nice eh? Of course you can use Ruby’s shellescape method to sanitize the input.

require 'shellwords'
user_input = "hello; rm -rf *"
output = `echo #{user_input.shellescape}`

But the problem is that you have to make sure that every developer never forgets to use the sanitizer. I can’t trust myself to never forget to do this, much less trusting that every developer will never forget.

So what are the alternatives?

There are a few Ruby built in methods that automatically handle this.

Kernel.system

Kernel.system runs the command and returns true if the command was successful (has 0 exit status). If you pass user_input as separate arguments, it will safely escape it.

system("echo", user_input)

Of course you can still pass the user_input interpolated in the command string and still make your code un-safe.

system("echo #{user_input}") # Don't do this please !!

But a static code analysis tool like codeclimate brakeman will catch this and warn you about the vulnerability.

$ codeclimate analyze -e brakeman myapp.rb

== myapp.rb (1 issue) ==
1-: Possible command injection [brakeman]

If you notice though, Kernel.system does not return the output of the command the way backtick does. This leads us to the next alternative, Open3.

Open3 module

From Open3 documentation:

Open3 grants you access to stdin, stdout, stderr and a thread to wait for the child process when running another program.

There are many methods provided by Open3 which you can read in the documentation. For the example we’ve been using, we can use Open3.capture2 to replace using backtick to run a command and capture the output.

user_input = "hello; rm -rf *"
output, status = Open3.capture2('echo', user_input)
puts output            # -> "hello; rm -rf *\n"
puts status.pid        # 123 or the process id
puts status.exitstatus # 0

Conclusion

It’s OK to use backtick when you don’t have any user input. But when you start passing untrusted input it’s best to always use system or Open3 to sanitize the input. It’s also strongly recommended to use a static code analysis tool like codeclimate brakeman to make sure no developer in your team forget to follow this best practice.