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 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." Databases, directory servers, cache servers, expression evaluators and language models all accept commands, and all of them can be made to read attacker input as one.
MITRE marks CWE-77 ALLOWED-WITH-REVIEW for vulnerability mapping, with the reason recorded as Frequent Misuse: "CWE-77 is often misused when OS command injection (CWE-78) was intended instead." Most findings labelled CWE-77 in practice are OS command injection and belong on CWE-78. Treat this page as a router: identify which interpreter the command reaches, go to that child for the concrete APIs, and use the guidance here only when the sink is a command language with no page of its own.
Relationship to Other CWEs
- CWE-77 (this page) - the parent for command injection into any command language. A Class-level weakness: it describes the shape of the bug, not the API that closes it.
- CWE-74 (Injection) - the parent of this page, and of the whole injection family. Too abstract to map a finding to.
- CWE-78 (OS Command Injection) - the child for operating-system commands, whether or not a shell is involved. This is the right page for nearly every finding a scanner reports as CWE-77.
- CWE-88 (Argument Injection) - the child for the narrower case where the attacker cannot change which program runs but can still inject an extra argument or flag. It survives the array-form fix that closes CWE-78.
- CWE-1427 (Improper Neutralization of Input Used for LLM Prompting) - the child where the command language is a model prompt.
- CWE-917 (Expression Language Injection) and CWE-624 (Executable Regular Expression Error) - the remaining children, neither with a page here. Both are the same weakness with a non-shell interpreter: Spring EL or OGNL for CWE-917, a regex engine's evaluate-the-replacement flag (Perl's
s///e, PHP's removed/e) for CWE-624.
OWASP Classification
A05:2025 - Injection
Risk
Critical: The impact depends entirely on what parses the command, so it is read off the sink rather than off this page. Against an OS shell the attacker runs programs with the application's privileges. Against a cache or database server they read, overwrite or destroy everything the connection is permitted to touch, and on some servers reconfigure it. Against an expression evaluator they reach the host language's runtime. In every case the attacker inherits what that connection or process was already allowed to do, which is why least privilege bounds the damage without closing the weakness.
Remediation Steps
Core Principle: Never build a command by concatenating untrusted input into its text. Use the interpreter's structured form, where the command and its data travel as separate elements, so nothing in the data can be read as syntax.
Trace the Data Path
- Source: Where untrusted data enters - a request parameter, an uploaded file, a database row, an upstream API response.
- Command construction: The string concatenation, format string or template that merges the value into the command text.
- Sink: The component that parses the result, and the command language it parses. Name it explicitly - the fix is a property of that interpreter, not of the calling code.
- Separator: The element that ends one command and starts the next in that language. It is
;and|in a POSIX shell, CRLF in Redis and SMTP, the parentheses in an LDAP filter, a newline in many device protocols. If you cannot name it, you cannot tell whether the input is neutralized. - Missing controls: Every point between source and sink where the value could have been kept out of the command text and was not.
Use the Interpreter's Structured Form (Primary Defense)
Almost every command language has a form in which arguments are delivered out of band rather than parsed out of a single string. Use it:
- OS commands: an argument array with the shell disabled - see CWE-78 for the API in each runtime.
- Cache and database servers: the maintained client library, which encodes commands in the server's own framed protocol rather than writing text to a socket.
- Directory servers: a filter builder, or the platform's filter-escaping function, rather than string concatenation.
- Expression evaluators: parse the expression from a fixed template and bind the untrusted value as a variable, or do not evaluate attacker-influenced expressions at all.
Why this works: a concatenated command is one string, and a string carries no record of where each character came from. The structured form gives the interpreter the boundaries directly - a length prefix, a discrete array element, a bound variable - so it never has to infer them from the bytes, and a separator sitting inside the data is just a byte.
Do Not Escape as the Primary Fix
Escaping keeps the single-string design and adds a rule the developer has to apply correctly at every call site, for the exact grammar of that interpreter, and still get right after something downstream decodes the value a second time. Where a structured form exists, reach for it instead. Escaping belongs only in legacy code that genuinely cannot use one, and then as a documented fallback rather than the control the fix rests on.
Add Input Validation (Defense in Depth)
Validate the value against the format the feature actually needs - an allowlist of permitted values, a strict pattern, or a parsed type such as an integer or an IP address. Fail closed: reject, do not attempt to clean. Validation is a second layer behind the structured form, never a replacement for it, because a validator has to be correct about a grammar the structured form never consults.
Test with the Sink's Own Separators
Send the separator for the command language you identified, not a generic payload list. A shell payload proves nothing about a Redis connection, and a CRLF proves nothing about an expression evaluator. Assert that each separator reaches the interpreter as literal data, and assert separately that legitimate input still works - a control that rejects every input passes every negative test.
Common Vulnerable Patterns
Redis is a clear non-shell case, because the protocol has two forms and only one of them is safe. A line that does not begin with * is parsed as an inline command: space-separated arguments, terminated by CRLF. An application that writes command text to the socket itself - a hand-rolled minimal client, a script piping text into a connection, anything avoiding a client-library dependency - is building in that form.
// VULNERABLE - pseudo-code, the note is concatenated into a Redis inline command
send(socket, "SET note:" + userId + " " + note + "\r\n")
// Attack: note = "x\r\nCONFIG SET dir /var/www/html\r\nCONFIG SET dbfilename shell.php\r\nSAVE"
// Redis then reads four commands where the application wrote one:
// SET note:42 x
// CONFIG SET dir /var/www/html
// CONFIG SET dbfilename shell.php
// SAVE
Why this is vulnerable: CRLF is the Redis protocol's terminator, so a CRLF inside note ends the SET and everything after it is read as a fresh command on the same connection, running with whatever that connection is allowed to do. No shell is involved and no OS command is constructed - this is CWE-77 and not CWE-78. The payload above is the documented CONFIG/SAVE chain: Redis's own security guidance notes that CONFIG "allows the client to change the working directory of the program and the name of the dump file. This allows clients to write RDB Redis files to random paths", which "may lead to the ability to compromise the system and/or run untrusted code as the same user as Redis is running." Even where that chain is blocked, the attacker still has FLUSHALL and read access to every key.
Secure Patterns
// SECURE - pseudo-code, the client library frames each argument with its byte count
redis.set("note:" + userId, note)
// What goes on the wire is a RESP array of length-prefixed bulk strings:
// *3\r\n$3\r\nSET\r\n$7\r\nnote:42\r\n$<len>\r\n<the note's bytes, CRLF and all>\r\n
Why this works: $<len> tells the server exactly how many bytes the argument occupies, so it reads that many and stops. It never scans the value looking for a terminator, and a CRLF inside note is stored rather than obeyed. Redis states the consequence plainly: "The Redis protocol has no concept of string escaping, so injection is impossible under normal circumstances using a normal client library. The protocol uses prefixed-length strings and is completely binary safe."
That is the shape of the fix for every command language on this page. The concrete call changes - an argument array for a process, a bound variable for an expression, a filter builder for a directory - but in each case the interpreter is told where the data ends instead of working it out from the data.
Common Pitfalls
- Treating "no shell" as "no command injection": the weakness is that an interpreter parses attacker-influenced text as syntax. A Redis server, an LDAP directory and an expression evaluator all qualify. Removing
sh -ccloses the shell case and says nothing about the others. - Writing protocol text by hand to avoid a dependency: a minimal in-house client that formats commands as strings gives up the framing that made the protocol safe. The client library is not overhead here; the length prefixes are the control.
- Reaching for an escaping helper first: a quoting function written for one grammar gets applied to another, or applied to a value that is decoded again downstream. Escaping is the fallback for legacy call sites, not the design.
- Validating the format but keeping the concatenation: a pattern that looks right today constrains the value, not the command structure. When the pattern is later widened, or the value takes a different route to the same sink, the injection returns with nothing in the code having been marked unsafe.
- Mapping every command-injection finding to CWE-77: MITRE records this as the reason CWE-77 is ALLOWED-WITH-REVIEW. If the sink is an OS process, the finding belongs on CWE-78; if only an argument to a fixed program is attacker-controlled, on CWE-88.