OS command injection
CWE-78OWASP A03:2021Updated August 31, 20266 min read
OS command injection is a vulnerability that lets an attacker run their own operating system commands, because user input reaches a system call by way of a shell. Characters such as a semicolon or a pipe start a second command there. The structural fix is to invoke the program with an argument array and no shell at all.
Plenty of applications shell out to an operating system program behind the scenes: a ping for a connectivity check, ImageMagick to resize an image, a converter that turns a document into a PDF. The moment user input reaches one of those calls without a strict separation between command and argument, you have OS command injection. Here is how it works and how to close it.
What is OS command injection?
OS command injection is a vulnerability that lets an attacker execute their own operating system commands on your server, because their input reaches a system call by way of a shell. OS stands for operating system: these are commands that run alongside your application on the host, with the privileges of the application process.
An everyday comparison: you hand a courier a note with an address on it. If the note holds only an address, the parcel gets delivered. If someone adds “and then hand over the warehouse key” after the address, a courier who works through the note line by line carries out that second instruction too. A shell is that courier: it reads a line of text and treats a character such as a semicolon as the start of a new instruction.
Those characters are known as shell metacharacters: the semicolon, the pipe, the ampersand, a newline, and the substitution forms written with backticks or with a dollar sign followed by parentheses. They separate commands or run one command inside another. As long as your input arrives at a shell as loose text, the shell cannot know you meant those characters as data.
Command injection is often mentioned in the same breath as remote code execution (RCE). Command injection describes the route: input reaches a shell and starts a second command there. RCE describes the outcome: arbitrary code running on your server, regardless of how it got there. Command injection is therefore one road to RCE; insecure deserialisation or an unrestricted file upload are others.
How does an OS command injection attack work?
Take an admin panel with a connectivity check. The user submits a hostname, the server pings it, and the output is shown back. The developer builds the command line as a string and hands it to the shell.
Vulnerable:
const { exec } = require("node:child_process");
app.get("/diagnostics/ping", (req, res) => {
const host = req.query.host;
// The entire line of text goes to /bin/sh
exec("ping -c 1 " + host, (err, stdout) => {
res.type("text/plain").send(stdout);
});
});
An ordinary user submits example.com and gets the expected result. An attacker follows the hostname with a semicolon and a command of their own. In the HTTP request it looks like this, with the semicolon URL-encoded as %3B:
GET /diagnostics/ping?host=example.com%3Bid HTTP/1.1
Host: app.example.com
The application concatenates that value onto ping -c 1 unchanged. What actually executes is this:
ping -c 1 example.com;id
The shell sees two instructions instead of one: it pings the host, then runs id, and the output of that second command lands in the response. From there, anything the application process is allowed to do is on the table: reading files, opening a reverse connection, pulling secrets out of environment variables.
The fix is to invoke the program directly rather than through a shell, passing a list of arguments. Every element of that list is one argument by definition, so punctuation inside it carries no special meaning. Node.js offers execFile for this, Python has subprocess.run with a list and without shell=True, and Java has the ProcessBuilder.
Secure:
const { execFile } = require("node:child_process");
app.get("/diagnostics/ping", (req, res) => {
const host = req.query.host;
// Allowlist: starts with a letter or digit, then only dots and hyphens
if (typeof host !== "string" || !/^[a-z0-9][a-z0-9.-]{0,253}$/i.test(host)) {
return res.status(400).send("Invalid hostname");
}
// No shell: host stays exactly one argument
execFile("ping", ["-c", "1", host], (err, stdout) => {
res.type("text/plain").send(stdout);
});
});
Now example.com;id is simply a hostname that does not resolve. There is no shell to interpret the semicolon, and the allowlist rejects the value at the front door anyway. The argument array strips the meaning out of the metacharacters, and validation limits what gets in at all.
What is the impact of OS command injection?
The impact is almost always severe, because the attacker leaves the application layer and lands on the host. They run commands with the privileges of the application process: reading configuration files and keys, pulling database passwords out of environment variables, copying source code. A second stage usually follows, since the server is a trusted point inside your network and may talk to internal systems that are unreachable from outside: databases, admin interfaces, your cloud provider’s metadata service.
In business terms that covers the full spectrum: data theft with a regulatory notification duty, data manipulation, service outage, or ransomware that uses this entry point to get in. Because severity depends on the privileges of the process and on what else the server can reach, the classification runs from high to critical. An injection into an unprivileged process in a confined container is manageable; the same flaw in a privileged process with unrestricted outbound traffic is not.
How do you detect OS command injection?
Start with functionality that smells like an external program: network diagnostics, thumbnail generation, document conversion, archive extraction, backups, and PDF rendering. In the code, look for the familiar calls: exec, system, popen, shell_exec, passthru, Runtime.exec, and any variant carrying an option to use a shell.
Testing means appending a separator and a harmless command to such a parameter, for example a semicolon followed by id, then the same idea with a pipe and with backticks. If the output never appears in the response, the case is blind. Then you work with a measurable side effect: sleep 10 while you time the response, or an outbound request to a domain you control, so your DNS log tells you whether the server reached out.
Scanners recognise the textbook cases but miss the injections that sit behind authentication or only fire in an asynchronous processing step. A thorough penetration test therefore combines automation with manual work and a look at the code. AssistSec covers this ground in a penetration test and demonstrates, per finding, which command actually ran.
How do you prevent OS command injection?
- Avoid the shell entirely. First ask whether that external program is needed at all; a library in your own language that does the same job never has this problem.
- Invoke programs with an argument array. Use
execFileorspawnin Node.js,subprocess.runwith a list in Python, and theProcessBuilderin Java. Never switch the shell option on. - Validate input against an allowlist. Describe what a valid value looks like and reject everything else. A blocklist of characters is always incomplete.
- Use indirect references. Let the user pick from a fixed list and translate that choice server-side into the real path or the real flag.
- Run with least privilege and restrict outbound traffic. An unprivileged account in a confined container makes the follow-up stage after an injection considerably harder.
- Log process creation and have the code tested. A web server that suddenly launches
curlorshdeserves investigation; a code review of system calls plus a targeted penetration test catches what scanners skip.
Sources
Frequently asked questions
What is the difference between command injection and remote code execution?
Command injection describes the route: input reaches a shell and starts an extra command there. Remote code execution describes the outcome: an attacker runs arbitrary code on your server, however they got there. Every command injection is a form of RCE, but not every RCE is command injection; insecure deserialisation or a vulnerable template engine reach the same result by a different path.
Is command injection the same as SQL injection?
The root cause is identical, the target is not. With SQL injection the input ends up in a database query; with command injection it ends up in the operating system shell. Command injection is usually the more severe of the two, because the attacker lands on the server itself rather than inside the database.
Does escaping user input prevent command injection?
Only partly. Helpers such as escapeshellarg in PHP quote a single value and are useful, but they do not help if you place the value outside those quotes, and they do not stop a value from being read as an extra option. Calling the program without a shell, passing a list of arguments, is far more reliable.
How do I test whether a parameter is vulnerable to command injection?
Append a separator and a harmless command to any parameter that looks like it feeds a system tool, for example a semicolon followed by id. If no output comes back, use a measurable side effect instead: a delay such as sleep 10 while you time the response, or a command that makes a DNS request to a domain you control.
Related articles
- GlossaryPentestA penetration test (pentest) is a controlled attack on your systems by ethical hackers. Learn how a pentest works and what vulnerabilities it uncovers.
- VulnerabilitiesCWE-22A01:2021Path traversal (directory traversal)Path traversal (directory traversal) explained: how attackers use ../ to escape the intended folder and read sensitive server files, and how to prevent it.
- VulnerabilitiesCWE-94A03:2021Remote code execution (RCE)Remote code execution (RCE) explained: how attackers run their own commands or code on your server through unvalidated input, and how to prevent it.
- VulnerabilitiesCWE-89A03:2021SQL injectionSQL injection explained: how attackers use unfiltered input to read or change your database, what the impact is, and how prepared statements stop it.
- VulnerabilitiesCWE-918A10:2021Server-side request forgery (SSRF)Server-side request forgery (SSRF) explained: how attackers abuse your server to reach internal systems and cloud services, and how to prevent it.