Skip to Content
Course content

77: Shellwords for Safe Shell Escaping

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're hiring a courier to deliver a package. You give them a set of written instructions: "Go to the warehouse, pick up the box labeled 'Fragile', and bring it to me." Everything is fine. But then, a prankster manages to change the label on the box to: "'Fragile' and then set fire to the warehouse."

If the courier follows instructions literally and blindly, they aren't just delivering a box anymore—they're executing a command to burn the building down. The courier doesn't realize that "set fire to the warehouse" was meant to be part of the name of the object, not a new instruction to follow.

This is exactly what happens when you pass raw user input into a shell command in Ruby. Your Ruby script is the manager, the shell (like bash or zsh) is the courier, and the user input is the label. If that input contains shell metacharacters—like semicolons, backticks, or pipes—the shell treats them as new commands rather than just a piece of text. That's where Shellwords comes in. It acts as the "quotation marks" that tell the shell, "Everything inside here is just a label; do not execute any of it."

The Shell Injection Trap

I've seen developers do this countless times: they want to let a user specify a filename to be processed by a system tool. It looks innocent enough:

filename = params[:filename] # Imagine this comes from a web form
system("ls -l #{filename}")

If a user provides my_file.txt, it works great. But if a malicious user provides my_file.txt; rm -rf /, your Ruby script effectively tells the shell: "List the details of my_file.txt, and then delete everything on the hard drive." The semicolon is the trigger that tells the shell to start a brand new command.

Neutralizing the Threat with Shellwords

The Shellwords module provides a simple way to "escape" these strings. Escaping is the process of adding backslashes or quotes around special characters so the shell treats them as literal text. Here is how you handle it properly:

require 'shellwords'

# Dangerous input from a user
user_input = "my_file.txt; whoami"

# Escape it!
safe_input = Shellwords.escape(user_input)

puts safe_input 
# Output: my_file.txt\;\ whoami

system("ls -l #{safe_input}")
# The shell now looks for a file actually named "my_file.txt; whoami" 
# instead of running the 'whoami' command.

By calling Shellwords.escape, you've effectively told the shell: "Treat this entire string as one single argument." The semicolon is now just another character in a filename, not a command separator.

Handling Arrays of Arguments

Sometimes you aren't just escaping one string, but a whole list of arguments. While you could map Shellwords.escape over an array, there's a cleaner way if you're building a full command string. I usually prefer using the array form of system or exec (which bypasses the shell entirely), but if you must build a string for a specific reason, you can use Shellwords.join.

require 'shellwords'

args = ["my file.txt", "some other file; rm -rf /"]
command = "ls -l " + Shellwords.join(args)

puts command
# Output: ls -l my\ file.txt some\ other\ file\;\ rm\ -rf\ /

One quick tip: if you find yourself using Shellwords constantly, it's a sign you might be relying too heavily on system calls. Whenever possible, use Ruby's built-in libraries (like File or Dir) to do the job. But when you absolutely have to step outside of Ruby and talk to the OS, never—and I mean never—trust a string that came from a user without escaping it first.




📋 Practical Task

Hardening a File Backup Script

You have been handed a legacy script that backs up a specific file to a backup directory using the cp command. Currently, the script is vulnerable to shell injection because it interpolates the filename directly into the system call.

Your Goal: Modify the backup_file method to use Shellwords so that the script can safely handle filenames that contain spaces, semicolons, or other shell-active characters.

require 'shellwords'

def backup_file(filename)
  backup_dir = "/tmp/backup/"
  
  # VULNERABLE LINE:
  # This is where the shell injection happens.
  # Fix this line using Shellwords.escape
  system("cp #{filename} #{backup_dir}")
end

# Test Case 1: Normal file
backup_file("notes.txt") 

# Test Case 2: Malicious input
# This should attempt to copy a file with this weird name, 
# NOT execute 'echo HACKED'
backup_file("notes.txt; echo HACKED") 

Ensure that the resulting command passed to system treats the malicious input as a single literal filename.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.