Overview
Command injection happens when an application builds an operating-system command out of untrusted input and hands it to a shell to run. To understand the flaw, you first have to understand the boundary you are crossing: the line between data (a value the program is working with) and code (instructions the shell will execute). This lesson explains what a shell does with a command string, why that string is really a small program, and where the dangerous boundary sits. Everything here is framed for authorized, isolated practice — a deliberately vulnerable lab app you own, running in a sandbox — never against systems you lack written permission to test.
Learning objectives
- Explain what a system shell does when it receives a command string.
- Distinguish the data a program intends to pass from the code a shell may interpret.
- Identify the shell metacharacters that mark the data/code boundary.
- Recognize why "the shell parses the whole string" is the root of command injection.
Core concept
A shell (such as /bin/sh or bash) is not a plain command runner — it is an interpreter. When you give it a string, it does not treat that string as one opaque command. It parses the string: splitting it into words, expanding variables, resolving globs, and — crucially — recognizing control operators that separate or chain multiple commands.
That parsing step is the whole story. A value that a developer thinks of as a single argument (a filename, a hostname, a search term) is, to the shell, just more text in the program it is about to interpret. If that text contains characters the shell treats as code rather than data, the shell will act on them. The boundary between "this part is a value" and "this part is an instruction" exists only in the developer's head — the shell does not know or respect it unless you make it explicit.
How it works
When an application runs something like a shell command string, the shell reads the entire line and looks for special characters, called metacharacters, that have meaning beyond their literal text. The common ones that create a data/code boundary include:
;— command separator: run one command, then the next.|— pipe: send one command's output into another command.&and&&— background / conditional-run operators.`and$(...)— command substitution: run a command and insert its output.><>>— redirection of input and output to files.*?[...]— globbing: expand to matching filenames.\n(newline) — also separates commands.
The shell processes these before running anything. So if a program builds a command by pasting user text directly into the command line, any metacharacters in that text change the structure of the command — adding new commands, redirecting output, or substituting other command results. The user's "data" has crossed the boundary and become "code."
The safe way to think about it: a shell command line is source code. Interpolating raw input into it is exactly like interpolating raw input into a SQL query or an HTML page — the same injection family, just a different interpreter.
Practical example
Imagine a lab tool that pings a host the user types in. A naive implementation builds the string ping -c 1 <user input> and hands the whole thing to a shell.
- Benign input
example.testproducesping -c 1 example.test— one command, as intended. - Input
example.test; idproducesping -c 1 example.test; id. The shell sees the;, treats it as a separator, and runs two commands: the ping, thenid. The second command was never intended by the developer.
Nothing "hacked" the ping program itself. The shell simply parsed a string that happened to contain a separator, and the boundary between the hostname (data) and a second command (code) was never enforced. In a sandboxed lab you can watch this happen safely; the lesson is why it happens, so you can prevent it.
[!NOTE] The point of this example is the mechanism, not a recipe. Keep experiments inside an isolated VM or container you own. The defensive takeaway — never build shell command lines from raw input — is developed across this module.
Cybersecurity relevance
Command injection is consistently ranked among the most severe web and application vulnerabilities, because a successful injection often means arbitrary command execution on the server — full compromise of that host's context. Understanding the shell boundary is the foundation for both sides:
- Defenders and developers learn to keep untrusted values out of any string the shell will parse, which is the single most reliable prevention.
- Testers, working under authorization, learn to reason about where input flows into command execution and how the shell would interpret metacharacters there.
The concept also transfers: SQL injection, template injection, and command injection are all the same "data became code because an interpreter parsed it" problem in different interpreters.
Common mistakes
- Thinking of a command string as one command. The shell may see many; metacharacters split it.
- Assuming quoting the input makes it safe. Quoting can help but is fragile and easy to get wrong; the robust fix is to avoid the shell entirely (later lessons).
- Treating the shell like a function call. A function receives discrete arguments; a shell parses one big string, which is why the boundary is so easy to lose.
- Filtering "bad characters" by blocklist. There are many metacharacters and encodings; enumerating the bad ones is error-prone.
Key takeaways
- A shell is an interpreter: it parses a command string into words, expansions, and multiple commands before executing.
- Metacharacters (
;,|,&,`,$(...), redirects, globs, newline) mark the boundary where data can turn into code. - Command injection is the result of that boundary being crossed: untrusted input placed into a string the shell parses.
- The reliable defense direction is to never build shell command lines from raw input — treat command construction like any other injection-prone interpreter.
Check your understanding
- Why is a shell command line better thought of as "source code" than as "a single command"?
- Which characters in a command string can cause the shell to run more than one command, and why does that matter for injection?

