Ruby
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Methods and Blocks
-
Section 4: Object-Oriented Ruby
-
Section 5: Metaprogramming
-
Section 6: Working with Data and Files
-
Section 7: Ruby Frameworks Overview
-
Section 8: Ecosystem and Testing
-
Section 9: Practical Projects
-
Section 10: Interview Practice
-
Section 11: Data Structures and Algorithms in Ruby
-
Section 12: More Practice Exercises
-
Section 13: Enumerable and Functional Style
-
Section 14: More OOP Practice
-
Section 15: More Testing
-
Section 16: Enumerable and Comparable Modules In Depth
-
Section 17: Ruby Standard Library: Core Utilities
-
Section 18: Ruby Standard Library: Data and Security
-
Section 19: Ruby Standard Library: CLI and Text
-
Section 20: Ruby Networking
-
Section 21: Ruby on Rails Deep Dive
-
Section 22: Ruby Metaprogramming Deep Dive
-
Section 23: Ruby Design Patterns
-
Section 24: Ruby Concurrency
-
Section 25: Ruby Testing Deep Dive
-
Section 26: Ruby Gems and Packaging
-
Section 27: Ruby Performance
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Rails API Development
-
Section 31: Rails Authentication and Authorization
-
Section 32: Rails Testing Deep Dive
-
Section 33: Rails Performance
-
Section 34: Rails Deployment
-
Section 35: Sinatra and Lightweight Ruby Web Apps
-
Section 36: More Ruby Language Deep Dive
-
Section 37: Ruby 3.x Modern Features
-
Section 38: More Data Structures in Ruby
-
Section 39: More Practical Projects
-
Section 40: Ruby Ecosystem Tools
-
Section 41: More Practice and Review
-
Section 42: Final Practice and Mastery
-
Section 43: Ruby Interview Deep Dive
-
Section 44: Ruby Background Processing Deep Dive
-
Section 45: Ruby GraphQL
-
Section 46: Ruby Object Model Deep Dive
-
Section 47: Ruby Hanami Framework Overview
-
Section 48: Ruby gRPC and Protocol Buffers
-
Section 49: Ruby Data Processing
-
Section 50: Ruby Search Integration
-
Section 51: Ruby File Upload and Media
-
Section 52: Ruby Email and Notifications
-
Section 53: Ruby Admin Panels
-
Section 54: Ruby Feature Flags and Experimentation
-
Section 55: Ruby Monitoring and Observability
-
Section 56: Ruby Docker and Deployment Deep Dive
-
Section 57: Ruby Security Deep Dive
-
Section 58: More Advanced Metaprogramming
-
Section 59: More Final Projects
77: Shellwords for Safe Shell Escaping
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.
There are no comments for now.