Command Injection Prevention
SQL Without Sleep Deprivation
Web risk is rarely mysterious. It usually lies in predictable mistakes that persist under time pressure.
In Command Injection Prevention, it's about strict input boundaries, parameterized queries and reviews that catch query risk early.
This makes security less of a separate afterthought check and more of a standard quality of your product.
Immediate measures (15 minutes)
Why this matters
The core of Command Injection Prevention is risk reduction in practice. Technical context supports the choice of measures, but implementation and embedding are central.
Defense: how to prevent this
Now let's get serious. Because all those attack techniques are nice and all, but if you're a developer (or managing developers), you need to solve this problem.
Rule 1: Don't use system calls
This is the only rule you really need. If you never call
os.system(), subprocess.call() with
shell=True, exec(), system(), or
any other function that executes a shell command with user
input, then you don't have command injection. Period. Done. End of
story.
"But I need to run a ping!" No, you don't. Use a
library. In Python: import ping3. In PHP: use
fsockopen() for network connectivity. In Java: use
InetAddress.isReachable(). There's always a library that
does what you want without having to invoke a shell.
"But I need to generate a PDF!" Use a library.
reportlab in Python, wkhtmltopdf via a
wrapper library, iText in Java. No reason to call
os.system("wkhtmltopdf " + filename).
"But I need to call ImageMagick!" Use the Python binding
Wand, or the PHP Imagick extension, or the Java
im4java library. They all call ImageMagick without
needing a shell.
Rule 2: If you must make a system call
Sometimes there really is no alternative. You need to call nmap,
or pandoc, or some obscure legacy tool with no
library available. In that case:
Use parameterized execution:
# WRONG:
os.system("ping -c 4 " + user_input)
# WRONG (shell=True):
subprocess.call("ping -c 4 " + user_input, shell=True)
# CORRECT:
subprocess.call(["ping", "-c", "4", user_input])The crucial parameter is shell=False (the default in
Python's subprocess). If you pass the arguments as a list
instead of a string, they are not interpreted by a shell.
The special characters ;, |,
&&, etc. are treated as literal
characters, not as operators.
In PHP:
// WRONG:
system("ping -c 4 " . $_GET['ip']);
// CORRECT:
$ip = escapeshellarg($_GET['ip']);
system("ping -c 4 " . $ip);
// BETTER:
$output = [];
exec("ping -c 4 " . escapeshellarg($_GET['ip']), $output);escapeshellarg() wraps the input in single quotes and
escapes all existing quotes. Not bulletproof, but much better than
nothing.
Rule 3: Whitelist input
Validate your input against a whitelist. If you expect an IP address, check whether the input actually looks like an IP address:
import re
def is_valid_ip(ip):
pattern = r'^(\d{1,3}\.){3}\d{1,3}$'
if not re.match(pattern, ip):
return False
parts = ip.split('.')
return all(0 <= int(p) <= 255 for p in parts)
user_ip = request.form.get('ip', '')
if not is_valid_ip(user_ip):
return "Invalid IP address", 400
subprocess.call(["ping", "-c", "4", user_ip])This is defense-in-depth. Even if there's a bug in how you invoke the command, an attacker can't do anything with it if the input is limited to valid IP addresses.
Rule 4: Least privilege
Don't run your web application as root. Run it as a restricted user with minimal rights. If someone does find command injection, at least they can't immediately take over the entire system.
In practice: a dedicated service account, no write permissions outside the application directory, no sudo rights, limited network access.
Rule 5: Sandboxing
Containers (Docker), seccomp profiles, AppArmor, SELinux -- use them. They don't prevent command injection, but they limit the damage. An attacker in a Docker container can do much less than an attacker on the bare metal host.
The uncomfortable truth
And now an honest word about developers who use
os.system().
I get it. I really get it. You have a deadline. Your manager wants
that feature live tomorrow. You need to quickly run a ping,
convert a file, generate a report. And
os.system("ping " + ip) is so damn easy. It works.
It does exactly what you want. In three lines of code you've got it
done, while the "proper" way with libraries and input validation and
subprocess with arguments-as-list takes three times as long.
But you know what's also easy? Leaving your front door wide open. That saves you five seconds every day looking for your keys. And on 364 days a year that works fine. But on that one day someone walks in who shouldn't be there, you have a problem. And then you say: "But it was so easy to leave the door open!"
It's the same logic. The same laziness. The same shortsighted
thinking that ensures we're still finding command injection
vulnerabilities in production applications in 2026. We've known since the
nineties how this works. We've known for thirty years how to prevent it.
And yet, every single time, a developer grabs
os.system() and concatenates user input onto it.
At this point it's not even incompetence. It's tradition. It's
a craft passed down from generation to generation: the art
of sloppy programming. Somewhere at a university sits a
professor teaching students how to use system(),
and forgetting to mention that it's a loaded gun.
And those students become developers. And those developers build
applications. And those applications run in production. And then we come
along with a semicolon and an id command, and then it's:
"Oh no, how is this possible? Who could have foreseen this?"
Everyone. Everyone could have foreseen this. Because it's in every security training, every OWASP list, every book on secure programming. It's probably also on the wall of the coffee room at the company you hired to do a pentest. But nobody reads that wall. Just like nobody reads the terms and conditions. Just like nobody reads the documentation.
And then they ask us: "Is it bad?"
Yes. It's bad. You've given someone root access to your server via a web form. That's like a bank drilling a hole in the vault door so customers can access their money more easily.
Reference table
Injection operators
| Operator | Syntax | Function | Platform |
|---|---|---|---|
| Semicolon | cmd1;cmd2 |
Execute both (regardless of result) | Linux |
| Ampersand | cmd1 & cmd2 |
Execute both (regardless of result) | Windows |
| Pipe | cmd1\|cmd2 |
Output cmd1 as input cmd2 | Both |
| AND | cmd1&&cmd2 |
cmd2 only if cmd1 succeeds | Both |
| OR | cmd1\|\|cmd2 |
cmd2 only if cmd1 fails | Both |
| Backtick | `cmd` |
Inline substitution | Linux |
| Dollar | $(cmd) |
Inline substitution | Linux, PS |
| Newline | %0a |
Command separation | Both |
URL encoding for HTTP parameters
| Character | Encoding |
|---|---|
& |
%26 |
\| |
%7c |
; |
%3b |
| Space | + or %20 |
' |
%27 |
" |
%22 |
` |
%60 |
$ |
%24 |
( |
%28 |
) |
%29 |
{ |
%7b |
} |
%7d |
\n |
%0a |
Space filter bypass techniques
| Technique | Example | How it works |
|---|---|---|
| IFS | cat${IFS}/etc/passwd |
Internal Field Separator |
| Input redirect | cat</etc/passwd |
Redirect as input |
| Brace expansion | {cat,/etc/passwd} |
Bash brace expansion |
| Hex space | X=$'\x20';cat${X}file |
Hex-encoded space in variable |
| Tab | cat%09/etc/passwd |
Tab as whitespace |
Keyword filter bypass techniques
| Technique | Example | How it works |
|---|---|---|
| Single quotes | c'a't file |
Empty quotes are stripped |
| Double quotes | c"a"t file |
Empty quotes are stripped |
| Backslash | c\at file |
Backslash before normal character |
| Wildcard | /bin/c?t file |
? matches one character |
| Variable | a=/etc/passwd;cat $a |
Value in variable |
| Base64 | echo aWQ=\|base64 -d\|bash |
Base64 decoding |
| Hex | echo -e '\x69\x64'\|bash |
Hex decoding |
Safe command execution validation
In production, use only defensive validation tests (allowlist validation, logging checks and error handling), without shell payloads or offensive test commands.
Defense measures
| Measure | Priority | Effectiveness |
|---|---|---|
| Don't use system calls | Critical | Eliminates the problem |
Parameterized execution (shell=False) |
High | Prevents operator interpretation |
| Input whitelisting | High | Limits attack surface |
escapeshellarg() (PHP) |
Medium | Escapes special characters |
| Least privilege | Medium | Limits damage |
| Sandboxing (Docker, seccomp) | Medium | Limits post-exploitation |
| WAF rules | Low | Bypassable, but slows down attacker |
Next chapter: Cross-Site Request Forgery -- or how to get someone else to do your dirty work.
Further reading in the knowledge base
These articles in the portal provide more background and practical context:
- APIs — the invisible glue of the internet
- SSL/TLS — why that padlock in your browser matters
- Encryption — the art of making things unreadable
- Password hashing — how websites store your password
- Penetration tests vs. vulnerability scans
You need an account to access the knowledge base. Log in or register.
Related security measures
These articles provide additional context and depth: