Fixing Command Injection Vulnerabilities in Ruby/Rails

brakeman, rails, ruby, security

This post details what a Command Injection vulnerabilitiy is, why you need to fix them, and how to fix them!

What is a Command Injection Vulnerability?

Command Injection is one of the worst types of security vulnerabilities that you can have within your system. It’s one part of a larger umbrella of vulnerabilities known as Injection vulnerabilities. Injection vulnerabilities have taken the #1 spot on the OWASP Top 10 in both 2010 and 2013.

*Using my best John Oliver voice*: Not something to be proud of Injection!

John Oliver yelling at an image that reads: Injection #1 in 2010 and 2013

Command Injection looks like this:

1
2
path = "#{Rails.root}/public/downloads/#{user_supplied_path}"
`ls #{path}`

That’s all that is required for an attacker to gain full access to your system. In such a case all of your data could be stolen, modified, or deleted. Not code that you want to be responsible for writing!

So how does an attacker leverage this code? Since the user is able to provide user_supplied_path they could do this:

1
2
3
4
user_supplied_path = '; cat ./config/database.yml'

path = "#{Rails.root}/public/downloads/#{user_supplied_path}"
`ls #{path}`  #=> ls /webserver/public/downloads; cat ./config/database.yml

This gives the attacker full access to the details of your database. At this point, your company goes bankrupt, you lose your job, and you stub your toe on the way out of your office. It’s a terrible day!

Let’s fix it.

How to Fix Command Injection Vulnerabilities in Rails/Ruby

There are multiple formats that a Command Injection vulnerability can come in based upon the system call that you’re making. Here’s some examples:

1
2
3
4
5
6
7
8
9
10
11
# All the below commands are unsafe!!

system("ls -a -l -@ -1 #{path}")                  # System
`ls #{params[:dir]} &2>1`                         # Backtick
%x(ls #{params[:dir]})                            # %x
exec("md5sum #{params[:input]}")                  # Exec

# Open3
Open3.capture2("ls #{params[:file]}")             # capture2
Open3.capture2e("curl -fsSL #{url}")              # capture2e
Open3.capture3("curl -fsSL #{url}")               # capture3

There are tons of commands that you could be using and thankfully they all follow the same pattern for properly protecting them from Command Injection.

To safely protect from Command Injection, call your command by breaking each piece into a seperate string. Here’s an example:

1
2
3
4
system("ls -a -l -@ -1 #{path}")

# Fixed
system("ls", "-a", "-l", "-@", "-1", path)

That’s it. Really simple! Now there are a few special cases that you’re going to run into, so let’s look at those.

Backtick (`) method

The backtick method is handled in a different way when writing system commands. Namely you cannot provide it multiple arguments. So instead when faced with command injection for backtick, you’ll need to use another method like system.

1
2
3
4
`ls -a -l -@ -1 #{path}`

# Fixed
system("ls", "-a", "-l", "-@", "-1", path)

Output Redirection

Output redirection is another special cases. Let’s say you’ve got a command like this:

1
`ls -a -l -@ -1 #{path} 2>&1`

Simple enough you think, and you change the backtick method to system:

1
system("ls", "-a", "-l", "-@", "-1", path, "2>&1")

The problem is you’re going to run into an error like this:

ls 2>&1: No such file or directory

This is because the system command now thinks that 2>&1 is a part of the arguments to ls and not an output redirection.

To fix this, you’re going to have to use a command from the Open3 library to properly handle output redirection. Because you’re using 2>&1 – which means redirecting stderr to stdout – you’re going to use capture2e:

1
2
require 'open3'
stdout_and_stderr, status = Open3.capture2e("ls", "-a", "-l", "-@", "-1", path)

And don’t forget to require 'open3' otherwise you’ll get:

NameError: uninitialized constant Open3

Logging

One pattern that I’ve seen is logging before running a system command like so:

1
2
3
cmd = "ls -a -l -@ -1 #{path}"
Rails.logger.debug("Running: #{cmd}")
`cmd`

I like to use the splat operator to simplfy the above to:

1
2
3
cmd = ["ls", "-a", "-l", "-@", "-1", path]
Rails.logger.debug("Running: #{cmd.join(" ")}")
system(*cmd)

File Access Vulnerabilities

One big caveat with all of the above is that there is still potential for abuse. Let’s say you’ve got the following command:

1
system("cat", user_supplied_path)

The above is a type of File Access vulnerability and brakeman isn’t going to recognize it!

In this case you’d want to switch to a Ruby built in command like File.read to grab the contents of the file, and make sure to sanitize the payload. Just remember to be careful that you don’t create a new vulnerability when fixing an old one!

This page was delicately crafted on by Gavin Miller.