OS Command Injection
Also known as: Command injection, Shell injection, OS command injection
A flaw in which untrusted input is placed into a command string run by the operating system shell, so the shell reads data as extra instructions; it is prevented by not using a shell and passing arguments as an array.
How it works
OS command injection is a flaw in how an application runs external programs. When the code builds one command string from fixed text plus a value the user supplied, and hands that string to the operating system shell, the shell parses the whole string. Input that should have been a plain argument can be read by the shell as additional instructions.
The root cause is the same as other injection flaws: code and data share one channel, and here the interpreter is the shell. It shows up in features that wrap system tools, such as network diagnostics, file conversion, archive handling, image processing and report generation. Impact depends on the privileges of the service account, from reading files the application should not touch to running arbitrary programs on the host and moving further into the network.
The fix is structural. Prefer a library API over launching a program at all. When a program must run, execute the binary directly without a shell and pass each argument as a separate array element, so the operating system never reinterprets the value. Escaping and blocklists are fragile and should never be the main defence.
Defence is layered. Direct execution with argument arrays is the primary control, strict allow-list validation of type and format narrows what reaches the call, a low-privilege service account with a restricted file system caps the damage, and process-tree monitoring on the host catches a web server spawning children it never normally starts.
Walk through it
- 1Read the vulnerable call
- 2Name the dangerous setting
- 3Read the signals, pick the fix
- Scope every process-execution call site
- Shell-free exec, allow-list and verify
A pre-release review flags the connectivity-check feature. An administrator types a hostname and the server tests whether it is reachable. Read the handler as a reviewer and note how the command is produced and who interprets it.
1# POST /api/diagnostics/reachability2import subprocess3 4def check_host(host):5 cmd = "ping -c 1 " + host6 result = subprocess.run(cmd, shell=True, capture_output=True, text=True)7 return result.stdoutSpot it
- A web or application server process spawning a shell or other child processes it does not normally start, visible in EDR process trees.
- Child processes of the service that are absent from its historical baseline, or run under unusual parent-child chains.
- A burst of HTTP 500 responses or command-failure errors from one source against a feature that wraps a system tool.
- Outbound network connections from the web server to destinations it never contacts, shortly after requests to that feature.
- Parameters on diagnostic or conversion endpoints that are longer or structurally different from normal hostnames or file names.
EDR process telemetry
ts=2026-10-11T10:02:11Z host=ntools-web-02 user=svc_webapp event=process_start parent=python3 child=sh note=unexpected_child_process
ts=2026-10-11T10:02:12Z host=ntools-web-02 user=svc_webapp event=process_start parent=sh child=unbaselined_binary note=not_in_30_day_baselineWeb access log
ts=2026-10-11T10:02:11Z src=203.0.113.77 method=POST path=/api/diagnostics/reachability status=500 bytes=412
ts=2026-10-11T10:02:13Z src=203.0.113.77 method=POST path=/api/diagnostics/reachability status=500 bytes=412kqlKQL: web service process spawning a shell
DeviceProcessEvents
| where InitiatingProcessFileName in~ ("python3", "node", "java", "w3wp.exe")
| where FileName in~ ("sh", "bash", "cmd.exe", "powershell.exe")
| summarize spawns=count() by DeviceName, InitiatingProcessFileName, FileNameAllow-list known maintenance jobs and correlate with web requests before escalating.
splSplunk: sources with a burst of server errors on a tool endpoint
index=app sourcetype=access path="/api/diagnostics/reachability" status=500
| stats count by src
| where count > 20Tune the threshold to the endpoint's baseline.
Stop it
Do not use a shell: run the program directly with an argument array
Prefer a library API over launching a program. When a program must run, execute the binary directly and pass each argument as a separate array element so no shell parses the value. Ban shell invocation helpers in code review and static analysis.
Validate input with strict allow-lists
Check type, length and format against what the feature needs, such as a hostname or address pattern, and map choices to a fixed list. Allow-listing narrows what reaches the call but does not replace removing the shell.
Run with least privilege and monitor process trees
Use a dedicated low-privilege service account with a restricted file system and egress, so a missed flaw has a small blast radius, and alert on web services spawning unexpected children.
Pass arguments as an array, no shell
Vulnerable
cmd = "ping -c 1 " + host
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)Hardened
import ipaddress
addr = ipaddress.ip_address(host) # allow-list: must parse as an IP address
result = subprocess.run(
["ping", "-c", "1", str(addr)],
shell=False, capture_output=True, text=True, timeout=5,
)Each argument is a separate element; the operating system never reinterprets the value.
ProcessBuilder with separate arguments
Vulnerable
Runtime.getRuntime().exec("ping -c 1 " + host);Hardened
ProcessBuilder pb = new ProcessBuilder("ping", "-c", "1", validatedAddress);
pb.redirectErrorStream(true);
Process p = pb.start();validatedAddress has already passed an allow-list check; no shell is involved.
- Ban shell-based execution helpers in coding standards and enforce it with static analysis in CI.
- Prefer native library functions over spawning external programs for ping, archive and conversion tasks.
- Execute binaries by absolute path with an argument array, a timeout and a minimal environment.
- Allow-list input format for every value that reaches a process call.
- Run the service as a dedicated low-privilege account with a read-only file system where possible and restricted egress.
- Alert in EDR when a web service process spawns a shell or an unbaselined child.
If it already happened
Disable or restrict the affected feature, block offending sources, and isolate the host if unexpected child processes or outbound connections are confirmed.
Replace every shell-based call on the affected path with direct execution and argument arrays, rotate credentials and secrets the service account could read, and review process and network telemetry to scope activity.
Rebuild the host from a known-good image if integrity is in doubt, run regression tests confirming no shell is used, and notify affected parties if regulated data was exposed.
Add a static-analysis rule and review checklist item against shell invocation, constrain the service account, and keep the process-tree detections permanently.
Check yourself
1. What is the root cause of OS command injection?
2. Which control is the primary fix?
3. Which signal best indicates a possible command injection against a web server?