Command Injection in Ruby:
Examples and Prevention

stackhawk

StackHawk|April 30, 2021

Let’s see what command injection is, how it works in Ruby, and understand how we can prevent command injection vulnerabilities.

Nowadays, websites suffer from a lot of hacking attempts. Hackers try different methods to get unauthorized access to your system. One such method is called command injection. It’s one of the most dangerous vulnerabilities that can be found on a website. It allows an attacker to execute any command on your host operating system. Possibilities are then endless—from reading the credentials for databases to deleting files or installing a backdoor. In this post, you’ll learn how a command injection works in general and how to prevent it in Ruby.  

What Is Command Injection?

Lets explore further what command injection is with an example of how it might happen in Ruby. Sometimes there’s a need for executing commands on the underlying operating system from a Ruby application. In general, it’s not recommended to do so. From time to time, however, you may, for example, need to run a third-party application that doesn’t expose its own API or any other way of triggering it. For these use cases, Ruby provides a few methods that allow you to execute commands on the operating system. The most popular are as follows: 

command
exec("command")
system("command")

For example, to create a folder, you could use something like this: 

system(mkdir "#{project_name}")

And while it’s recommended to avoid doing so, it doesn’t create any security vulnerability on its own. The problem arises when you pass the user input directly to such a command. For example, if you create a directory for a user-defined project, a vulnerable code could look like this: 

system(ls "#{params[:project_name]}")

So what happens here, and why does it create a command injection vulnerability? As you can see, we want to show the user their own files that are created in a specific project folder. Since these files belong to the user, we want to give them the functionality of specifying which project they want to see files from. For that, you use a standard Ruby form helper, which generates an input field on your website where the user can type the project name: 

<%= f.input :project_name %>

Now, we expect the user to input a project name, but what if they input anything else? For example, what if instead of my_project, they type && rm -rf / ? The command Ruby will execute will look like this: 

system(ls && rm - rf /)

And all your files are gone. 

Command Injection in Real Life

You may be thinking, “Yeah, OK, but I would never process files in such a manner.” And yes, we agree. The above example was unlikely to be seen in a real-life application. It was just an example to show you how the command injection works. However, there are more realistic scenarios, less obvious to avoid. For instance, the very popular library ImageMagick executes operating system commands under the hood. Millions of Rails applications use ImageMagick to manipulate images. Many of these allow the user to specify, for example, the size to which they want to resize the image. And here we go—there’s a command injection possibility. If you do it wrong and instead of “2000×1000” (pixels) the user inputs something else, Ruby will execute it through ImageMagick as an operating system command. Even such well-maintained libraries like Rake can be prone to command injection vulnerability. Looking at this CVE, you can see that Rake’s FileList method allowed an attacker to execute malicious commands on the OS. Enough intimidation. Let’s talk about preventing command injection in Ruby. 

Preventing Command Injection

First things first, let us make it clear: you should avoid executing commands on the operating system directly. Usually, there are other ways of achieving the same result. When you really can’t avoid it (for example, when using ImageMagick), then these are the methods for preventing a command injection. 

Parametrize the User Input

The first option is a Ruby best practice that can help you avoid not only command injection but some other attacks too. And it’s as simple as never using user input directly. Always sanitize and/or parameterize the user input. The same method will prevent you from a SQL injection attack. So what does it mean to sanitize or parameterize the output? Instead of doing this: 

system(ls #{params[:project_name]})

You should break the whole command into separate parameters: 

system("ls", "params[:project_name]")

As simple as that. And realistically, in this specific scenario (file and directory handling), you should use Ruby’s FileUtils module instead. The only problem with that method is that it doesn’t work when using the backticks method. This won’t work: 

`"ls", "params[:project_name]"`

So if you’re using backticks, you should switch to system() or exec() methods. 

Allowed Values

Another option that can be used on top of the previous one is to validate user input or even create allowed values. For the previous example, you could get the user’s project names first from the database, and for the params[:project_name], create a list of allowed values based on that. This way, if the user inputs anything other than the actual project name, it will be rejected. When you can’t build the list of allowed values because you want to give the user more freedom, you can use regex. For example, if you expect the user to only pass the name of something, then only allow the use of alphanumeric characters. If you only expect to get a number, then only allow numbers. This way, you block the attacker from using ” ; ” or ” && ” which are needed to chain the commands and therefore perform a command injection attack. This method can be used to avoid the previously mentioned ImageMagick command injection vulnerability. By using the following regex rule, you only allow the user to provide a valid image size: 

IMAGE_SIZE = /\A\d*x\d*[><%^!]?\z|\A\d+@\z/ #e.g. '2000x1000!'

stderr, stdout

When executing operating system commands from Ruby, you probably need to get the stdout and stderr streams from that command. This created a little bit of a problem when using the above command injection prevention methods. Simply specifying “2&>1” as another string in the list for system() method won’t work. If you need that functionality, you can use Ruby’s Open3 module, specifically its capture methods. So your command would look like this: 

Open3.capture2e("ls", "params[:project_name]")

Summary

Now you know how to prevent command injection in Ruby. You should be aware that with other vulnerabilities like cross-site scripting (XSS) or SQL injection, the attacker has limited possibilities. Of course, SQL injection, for example, is still pretty serious. It can allow an attacker to download all your users’ data and then (possibly) delete the database.

But with command injection vulnerabilities, attackers don’t even need to look for a SQL vulnerability. They can simply execute cat ./config/database.yml and get credentials to your database. So, while command injection is less common these days, it gives almost unlimited possibilities.

Fortunately, as you could see in this post, one of the most destructive vulnerabilities can be avoided pretty easily. In most cases, all you need to do is to split the command into separate strings. Nowadays, executing operating system commands from Rails should also be easily avoidable. There are many Rails modules and gems that you should be able to use instead and achieve the same results. If you want to learn more about command injection, you can read about it here. 

This post was written by Dawid Ziolkowski. Dawid has 10 years of experience as a Network/System Engineer at the beginning, DevOps in between, Cloud Native Engineer recently. He’s worked for an IT outsourcing company, a research institute, telco, a hosting company, and a consultancy company, so he’s gathered a lot of knowledge from different perspectives. Nowadays he’s helping companies move to cloud and/or redesign their infrastructure for a more Cloud Native approach.


StackHawk  |  April 30, 2021