Command injection is one of the oldest and most dangerous web bugs, and it is also one of the easiest to understand once you see it in action. It happens when an app takes input from a user, drops that input into a system command, and runs the whole thing in a shell. If the app trusts the input too much, the user can append their own commands and make the server run them.
What command injection means
The short version of the command injection meaning is this: your app wanted to run one command, but the attacker tricked it into running two. The first is the command you intended. The second is whatever the attacker tacked on. The shell happily runs both because, to the shell, it is just text.
The root cause is mixing two things that should stay apart: data (the value a user typed) and code (the command the server runs). When user data flows straight into a command string, the data can change what command runs. That is the whole bug in one sentence.
The app meant to run one command. The attacker made it run two. The shell cannot tell your intent from their input, so it runs both.
A simple command injection example
Let us invent a small app called Acme Netcheck. It is a network tool with one feature: you give it a hostname, and it pings that host so you can see if the host is reachable. The form has one field named host, and the backend runs a ping for you.
Here is the kind of code that causes the problem. This is written to show the mistake, not to copy:
# DANGEROUS: user input goes straight into a shell command host = request.form["host"] command = "ping -c 1 " + host output = os.popen(command).read() return output
If a normal user types example.com, the server builds and runs this:
ping -c 1 example.com
That works as intended. The trouble starts when someone types something that is not just a hostname. On a typical shell, a semicolon ends one command and starts another. So an attacker types this into the same field:
example.com; whoami
Now the server builds and runs this:
ping -c 1 example.com; whoami
The shell runs the ping, then runs whoami, and the app returns the output of both. The attacker just learned which user the web server runs as. They did not break into anything clever. They only added a semicolon and a second command to a field that was supposed to hold a hostname.
Other command injection examples that work the same way
The semicolon is one of several shell characters that chain or redirect commands. These all let an attacker smuggle a second command into a single input field:
example.com && whoamirunswhoamionly if the ping succeeds.example.com | whoamipipes the first command into the second.$(whoami)or`whoami`runs the inner command and pastes its result back in.
These are command injection examples you will see again and again because the cause is identical every time: input was treated as part of a command instead of as plain text.
Why command injection is so serious
With SQL injection, an attacker reaches your database. With command injection, the attacker reaches the operating system itself, running as whatever user your app runs as. That is a wider blast radius. Once they can run shell commands on your server, they can:
- Read files the app can read, including config files and secrets like API keys and database passwords.
- Reach other machines on the internal network that the server can talk to but you cannot reach from outside.
- Install a backdoor or a reverse shell so they can come back later.
A field meant to hold a hostname turned into full control of a server. That is why this bug class sits near the top of every serious security list.
How to fix command injection
The strongest fix is to stop building shell command strings out of user input. Most of the time you do not need a shell at all.
Do not shell out when an API exists
If you only need to read a file, use the file API in your language. If you need to make an HTTP request, use an HTTP library. Reaching for a shell command to do a job your language already does is the start of most of these bugs. No shell means no shell injection.
If you must run a program, pass arguments as a list
When you genuinely need to run an external program, call it directly and pass each argument as a separate list item instead of as one big string. Most languages support this. In Python it looks like this:
# Safer: no shell, arguments passed as a list
import subprocess
host = request.form["host"]
output = subprocess.run(
["ping", "-c", "1", host],
capture_output=True, text=True
).stdout
Here host is handed to ping as a single argument. There is no shell to interpret the semicolon, so example.com; whoami is passed to ping as one odd hostname, which fails to resolve. The second command never runs.
Validate input with an allowlist
Defense in depth helps too. Decide exactly what valid input looks like and reject everything else. For a hostname, you can allow only letters, digits, dots, and hyphens, and reject anything else before the value goes near a command:
import re
host = request.form["host"]
if not re.fullmatch(r"[A-Za-z0-9.-]+", host):
return "Invalid host", 400
An allowlist describes what you accept. A blocklist tries to list every bad character and always misses some. Prefer the allowlist.
Lower the impact when things go wrong
Run the app as a low privilege user, not as root. Limit what that user can read and which machines it can reach. None of this fixes the bug, but it shrinks the damage if one slips through. You can read more patterns like this in our guide to injection and input bugs.
How to spot it in your own code
Search your codebase for the places where commands get run. Look for os.system, os.popen, subprocess calls with shell=True, backticks, exec, and eval. For each one, ask a single question: does any part of this command come from a request, a form, a URL, a header, or a file an outside user can influence? If yes, treat it as suspect and fix it with the steps above.
Command injection survives because the dangerous code reads as harmless. Joining a string and running it looks fine in review. The bug only shows when someone tries the input you did not expect. This is exactly the kind of assumption an autonomous researcher that tests how an app really behaves is built to find. To see how we think about bugs like this, read more about UnboundCompute.









