Web Security

What Is Command Injection? OS Command Injection Explained

A complete technical guide to OS command injection: how untrusted input turns into arbitrary shell execution, and how to detect, verify, and prevent it.

Intermediate12 min·Nyxeara Security Research·2026-09-16·CWE-78
command-injectionweb-securityrceowasp-top-10
Prerequisites
what-is-httpwhat-is-a-shell

Short answer

OS command injection happens when an application passes user-controlled input into a system shell command, and the shell's own control characters let an attacker chain in commands the application never intended to run.

The idea in one minute

Picture a shop assistant who takes your spoken request and, rather than looking anything up themselves, literally retypes your exact words at a terminal and presses enter. You say "socks," they type lookup socks. It works fine — until you say "socks; delete inventory," and the terminal — which treats ; as "run this command, then run the next one too" — does exactly that. The assistant didn't misbehave. They followed instructions perfectly. The terminal just never distinguished between "the thing you're looking up" and "a second command you're also issuing."

That's command injection. The "terminal" is a system shell; the "assistant" is application code that builds a command string from user input and hands it to that shell. Shells have always supported chaining (;, &&, |) and substitution (` `, $()) — that's a feature, not a bug, for legitimate shell scripting. The vulnerability is letting untrusted input reach that shell at all.

How command injection actually works

Application code often shells out to existing command-line tools instead of reimplementing their functionality — running ping, converting an image, checking a DNS record. When that command is built by string concatenation and executed through a shell, every shell metacharacter in the input is interpreted by the shell, not treated as a literal value.

| Category | What happens | How it's confirmed | |---|---|---| | Classic/in-band | Injected command's output appears directly in the response | Output visible on-page | | Blind | No output returned, but the command still executes | Time-delay (sleep) or out-of-band callback | | Argument injection | Can't chain a new command, but can inject additional flags/arguments into the fixed command | Behavior change from an unexpected flag (e.g., pointing an output path somewhere new) |

The request flow

sequenceDiagram
    participant Attacker
    participant App as Application
    participant Shell as OS Shell

    Attacker->>App: Input containing shell metacharacters
    App->>Shell: Concatenated command string
    Shell-->>App: Executes BOTH the intended command and the injected one
    App-->>Attacker: Output reflected, or effects observed out-of-band

A minimal example

A "ping this host" diagnostic feature:

import os
from flask import request

@app.route("/ping")
def ping():
    host = request.args.get("host")
    output = os.popen(f"ping -c 4 {host}").read()   # shell interprets metacharacters
    return output

A request with host=8.8.8.8; cat /etc/passwd runs the ping and dumps the password file into the response, because the shell sees two commands separated by ;.

The fix isn't sanitizing every dangerous character — it's not invoking a shell at all:

import subprocess
import re
from flask import request, abort

@app.route("/ping")
def ping():
    host = request.args.get("host")
    if not re.match(r'^[a-zA-Z0-9.\-]+$', host):
        abort(400)
    result = subprocess.run(["ping", "-c", "4", host], capture_output=True, text=True)
    return result.stdout

Passing arguments as a list to subprocess.run (without shell=True) sends them directly to the ping program — there's no shell in between to interpret ;, |, or ` `, so those characters are just inert text as far as ping is concerned. The allowlist regex adds a second layer on top.

Why the vulnerability exists

  • Application code shells out to CLI utilities for convenience instead of using native library equivalents
  • APIs like os.system, os.popen, or subprocess.run(..., shell=True) are used, all of which invoke a shell that parses metacharacters
  • Commands are built via string concatenation instead of passed as argument arrays
  • Input that "looks like" a hostname, filename, or simple identifier is assumed safe without strict validation

What attackers look for

  • Diagnostic or utility features that clearly shell out: ping/traceroute/DNS lookup tools, "test this connection" buttons
  • Media processing pipelines that wrap external binaries (image conversion, video transcoding, PDF generation)
  • File compression/extraction features
  • Any endpoint whose behavior strongly resembles "take this input and run a system tool against it"

Detection

  • Static/code review: search for shell-invoking calls (system(), popen(), exec(), subprocess with shell=True, backticks) fed by request data without a strict allowlist.
  • Dynamic/DAST: for visible-output endpoints, inject a benign marker command (e.g., appending ; echo <unique-string>) and check whether it appears in the response; for blind cases, inject a time-delay command (; sleep 6) and measure response time against a baseline.
  • Out-of-band testing: where no output or timing signal is available, trigger a DNS/HTTP callback via an injected command to infrastructure you control.

Verification: real vulnerability or false positive?

Confirmed when the injected metacharacters produce a result that can only be explained by shell execution:

  • Command output appears that matches the injected marker, not the intended command's normal output
  • A time-based payload produces a delay matching the specified duration, repeatably, with a control request (no delay clause) showing no such delay
  • An out-of-band interaction is received, uniquely tied to the specific payload sent

A single slow response is not evidence — network jitter and server load produce the same symptom. Repeat the timing test and compare against an unmodified baseline before calling it confirmed.

Real-world impact

Command injection converts directly into remote code execution as the application's process user — there's no additional chaining required, which is why it's consistently ranked among the most dangerous vulnerability classes by organizations like CISA and MITRE. Consequences typically include full server compromise, credential theft from the compromised host, lateral movement into internal networks, and persistence (planting a backdoor) — the ceiling is "whatever the application's OS user account is allowed to do."

Prevention

  • Avoid invoking a shell entirely: use library APIs or subprocess-style calls with argument arrays instead of shell command strings — this removes the shell's metacharacter interpretation from the equation completely
  • Hardcode the command and its required flags: never let user input choose which executable runs or which required options are passed
  • Strict allowlist validation on any input that must reach a command: only permit the specific character set a legitimate value could contain (e.g., a hostname pattern)
  • Least privilege: run the process under an account with minimal OS permissions, so a successful injection has a limited blast radius
  • Keep dependencies current: some injection paths come through vulnerable third-party libraries that shell out internally, not just first-party code

Related vulnerabilities

  • SQL injection — the same "user input becomes executable syntax" root cause, applied to a database query language instead of a shell
  • Server-Side Template Injection (SSTI) — another interpreter-confusion vulnerability class
  • Path traversal — often chained alongside command injection to reach or overwrite specific files
  • Argument injection — the narrower case where new arguments, not new commands, get injected into a fixed executable

Testing methodology (do this safely)

  • Only test targets you're authorized to test.
  • Prefer time-based blind confirmation (sleep) over commands that read, modify, or exfiltrate real data.
  • Never run destructive commands (file deletion, service disruption) outside an explicitly authorized, isolated environment.
  • Document the exact payload, and the output or timing evidence that confirms execution.

Further reading

Nyxeara perspective

Blind command injection is only as trustworthy as its timing evidence. A scanner reporting a finding off a single slow response, without a repeated measurement against a clean baseline, will produce exactly the false positives that make automated results hard to trust — the sleep duration has to show up reliably, tied to the specific payload, before it counts as confirmed.

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