jan-karel.com
Home / Security Measures / Web Security / Command Injection Prevention

Command Injection Prevention

Command Injection Prevention

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.

Op de hoogte blijven?

Ontvang maandelijks cybersecurity-inzichten in je inbox.

← Web Security ← Home