VNX-C-003 – OS Command Injection via system() or popen() with Non-Literal Argument
Overview
This rule reports calls that pass a command string to a shell (system, popen, _popen, _wpopen, _wsystem, p2open, std::system, and execl/execlp/execle of sh/bash/zsh/dash with -c) when the command is argv[...] or getenv(...), a variable read from getenv, argv, readline, fgets, read, recv, fread, getline or scanf in the 25 lines above, or a variable composed in those lines from non-constant data (sprintf/snprintf/asprintf with a %s argument, strcat/strcpy of a non-constant, C++ std::string concatenation, append() or <<). It also reports wordexp() of a non-literal without WRDE_NOCMD. It does not report literal or macro commands, system(NULL), commands built only from literals and %d numbers, a command that is a parameter of the enclosing function, a command preceded by validation or quoting (is_valid_*, sanitize, escape, g_shell_quote, strspn/strpbrk), exec* without a shell, posix_spawn, prototypes, string literals and comments.
All four of these functions pass their argument to a shell for interpretation. system() and popen() invoke /bin/sh -c <command>, p2open() is a Solaris extension that does the same, and wordexp() performs shell-style word expansion including variable substitution and command substitution. When any portion of the command string derives from external input — user-supplied data, environment variables, filenames read from the filesystem, HTTP headers, or network packet fields — an attacker who controls that portion can inject shell metacharacters (;, |, &&, $(...), `) to execute arbitrary commands.
Severity: Critical | CWE: CWE-78, CWE-88, CWE-676
Why This Matters
OS command injection is consistently ranked in the OWASP Top 10 and requires no memory corruption knowledge to exploit — semicolons and pipes are the only primitives needed. A single unguarded system() call that incorporates network-supplied data can give an attacker full remote code execution at the privilege level of the running process.
Real-world examples:
- CVE-2014-6271 (Shellshock) — CGI programs that ran Bash as part of request processing passed HTTP headers into environment variables. Bash’s bug meant function definitions stored in environment variables could include trailing commands that executed unconditionally. Web servers that invoked CGI scripts via
system()orpopen()were trivially exploitable: an attacker sent a craftedUser-AgentorRefererheader and achieved remote code execution on any CGI-enabled server running a vulnerable Bash. Exploitation was automated within hours of disclosure and affected millions of servers. - CVE-2018-1000861 (Jenkins) — A Groovy sandbox bypass in Jenkins’ Stapler framework led to a
Runtime.exec()call (Java’s equivalent ofsystem()) being reachable with attacker-controlled arguments, giving unauthenticated RCE. - CVE-2006-3747 (Apache mod_rewrite) — An off-by-one error in
mod_rewritecombined withsystem()calls in CGI scripts that incorporated rewritten URLs led to command injection.
Embedded systems and network appliances are especially affected because they frequently use system() and popen() as quick wrappers around CLI tools (ping, iptables, ifconfig) and process user input from web interfaces directly. These patterns map to CAPEC-88 (OS Command Injection) and ATT&CK T1059.
SEI CERT C rule: ENV33-C — Do not call system().
OWASP ASVS v4.0: V5.2.4 — Verify that the application avoids the use of eval() or other dynamic code execution features. Where there is no alternative, any user input being included must be sanitized or sandboxed before being executed.
What Gets Flagged
// FLAGGED: command composed from a caller-supplied host name
char cmd[256];
snprintf(cmd, sizeof(cmd), "ping -c 1 %s > /dev/null", host);
return system(cmd);
// FLAGGED: command assembled with strcat
strcpy(cmd, "grep -c ");
strcat(cmd, pattern);
fp = popen(cmd, "r");
// FLAGGED: argv, the environment and stdin straight to the shell
system(argv[1]);
return system(getenv("POST_HOOK"));
while (fgets(line, sizeof(line), stdin) != NULL)
system(line);
// FLAGGED: C++ string concatenation
std::string cmd = "tar -xf " + archive + " -C " + dest;
return std::system(cmd.c_str());
FILE *p = popen(("ls " + dir).c_str(), "r");
Not flagged: system("date"), popen("uname -r", "r"), execvp(argv[0], argv) (no shell), and a command whose input passed is_valid_hostname(host) first.
Remediation
The preferred fix is to avoid shell-invoking functions entirely and use execve/execvp with an argument array.
When arguments are passed as an array to execve or execvp, they are delivered directly to the target program without involving a shell. Shell metacharacters in the arguments are treated as literal characters — injection is structurally impossible regardless of the argument content.
// SAFE: execvp — no shell, arguments are passed directly to the program
char *const args[] = {
"convert",
user_filename, // passed as a literal argument; ";" is just a character
"output.png",
NULL
};
execvp("convert", args);
// Note: execvp replaces the current process; call fork() first if you need to continue
// SAFE: fork() + execvp() + pipe to replace popen()
int pipefd[2];
if (pipe(pipefd) == -1) { handle_error(); }
pid_t pid = fork();
if (pid == 0) {
// Child: connect write-end of pipe to stdout, exec grep
close(pipefd[0]);
dup2(pipefd[1], STDOUT_FILENO);
close(pipefd[1]);
char *const grep_args[] = {"grep", "--", search_term, "/var/log/app.log", NULL};
execvp("grep", grep_args);
_exit(1); // execvp failed
}
// Parent: read from pipefd[0]
close(pipefd[1]);
If system() or popen() cannot be avoided:
- Build an allowlist of the exact literal commands permitted and reject anything not on the list.
- Use
snprintfto construct the command and then validate the result matches an allowlist pattern before callingsystem(). - Never interpolate user-supplied filenames, hostnames, search terms, or identifiers directly into a shell command string without strict character allowlisting (alphanumerics and limited punctuation only, rejecting any shell metacharacter).
Compiler and runtime hardening (not a substitute for the fix above):
-D_FORTIFY_SOURCE=2 # catches some dangerous uses of libc functions
-fsanitize=address # AddressSanitizer for test builds
Privilege reduction: ensure the process calling system() or popen() runs with the minimum necessary OS privileges. An OS command injection vulnerability in a low-privilege process causes less damage than one running as root.
References
- CWE-78: Improper Neutralisation of Special Elements Used in an OS Command
- CWE-88: Argument Injection
- SEI CERT C – ENV33-C: Do not call system()
- CAPEC-88: OS Command Injection
- OWASP – OS Command Injection Defense Cheat Sheet
- CVE-2014-6271 – Shellshock (NVD)
- Huntress – Shellshock CVE-2014-6271 Analysis
- ImmuniWeb – OS Command Injection / CWE-78