flawopen.com/Command Injection/Ruby
Imagine an intercom where you state your name to enter. If someone says 'Alice and open all doors', command injection is the intercom unlocking all doors because it failed to parse where the name ended.
Ruby provides multiple ways to execute commands: backticks (`cmd`), %x{cmd}, system(), exec(), and IO.popen(). If passed a single string containing user data, Ruby invokes /bin/sh.
Ruby on Rails applications have faced critical CVEs when file upload controllers called open() on user filenames, executing commands embedded in file paths.
CVE-2019-5477 & Rails Security Advisory.# Backtick execution invokes /bin/sh
def fetch_git_log(branch)
# Input: "main; rm -rf /"
`git log #{branch}`
end
# Multiple arguments to system/Open3 bypass the shell
require 'open3'
def fetch_git_log(branch)
# branch is passed as an isolated argument to git
stdout, stderr, status = Open3.capture3("git", "log", branch)
stdout
end
When Open3.capture3 or system receives multiple arguments, Ruby bypasses /bin/sh and directly calls execve.
In Ruby, open("| #{filename}") executes the filename as a shell command. Always use File.open() instead of Kernel#open or URI.open.
While Shellwords.escape helps, developers often forget it on some variables. Using multiple arguments in Open3 is immune to forgetting.
grep -rn '`.*#\{' --include="*.rb" .
grep -rn 'Kernel\.open\|URI\.open' --include="*.rb" .
Open3.capture3("cmd", *args) with discrete argumentsopen() on user-supplied filenames; use File.open()Because if a user submits a filename starting with '|', Ruby treats it as a pipe to a command. File.open() never has this behavior.