The C function system() executes a specified command by invoking an implementation-defined command processor, such as a UNIX shell or CMD.EXE in Windows NT and later. The POSIX popen() function also invokes a command processor but creates a pipe between the calling program and the executed command, returning a pointer to a stream that can be used to either read from or write to the pipe [Open Group 2004].
External programs are commonly invoked to perform a function required by the overall system. This is a form of reuse and might even be considered a crude form of component-based software engineering.
Command interpreters, such as the POSIX shell sh and the Microsoft Windows command-language interpreter CMD.EXE, however, provide functionality in addition to executing a simple command. If this functionality is not required, it is a good idea to avoid the system() function or any other function that invokes a command interpreter because such functions significantly complicate the command-string sanitization. (See ENV03-C. Sanitize the environment when invoking external programs.)
Noncompliant Code Example
In this noncompliant code example, the system() function is used to execute any_cmd in the host environment. Invocation of a command processor is not required.
char *input = NULL;
/* input gets initialized by user */
char cmdbuf[512];
int len_wanted = snprintf(
cmdbuf, sizeof(cmdbuf), "any_cmd '%s'", input
);
if (len_wanted >= sizeof(cmdbuf)) {
perror("Input too long");
}
else if (len_wanted < 0) {
perror("Encoding error");
}
else if (system(cmdbuf) == -1) {
perror("Error executing input");
}
If this code is compiled and run with superuser privileges on a Linux system, for example, an attacker can create an account by entering the following string:
happy'; useradd 'attacker
The shell would interpret this string as two separate commands
any_cmd 'happy'; useradd 'attacker'
and create a new user account that the attacker can use to access the compromised system.
Please note that this example also violates STR02-C. Sanitize data passed to complex subsystems.
Compliant Solution (POSIX)
In this compliant solution, the call to system() is replaced with a call to execve(). The exec() family of functions can be used to run external executables in a variety of ways depending on the function and parameters used.
The execlp(), execvp(), and (nonstandard) execvP() functions duplicate the actions of the shell in searching for an executable file if the specified file name does not contain a slash "/" character. As a result, they should be used without a slash character only if the PATH environment variable is set to a safe value, as described in ENV03-C. Sanitize the environment when invoking external programs.
The execl(), execle(), execv(), and execve() functions do not perform path name substitution.
The exec() functions do not use a full shell interpreter, so they are not vulnerable to command-injection attacks, such as the one illustrated in the noncompliant code example.
Additionally, precautions should be taken to ensure that the external executable cannot be modified by an untrusted user, for example, by ensuring the executable is not writable by the user.
char *input = NULL;
/* input gets initialized by user */
pid_t pid;
int status;
pid_t ret;
char *const args[3] = {"any_exe", input, NULL};
char **env;
extern char **environ;
/*... Sanitize arguments ... */
pid = fork();
if (pid == -1) {
perror("fork error");
}
else if (pid != 0) {
while ((ret = waitpid(pid, &status, 0)) == -1) {
if (errno != EINTR) {
perror("Error waiting for child process");
break;
}
}
if ((ret != -1) &&
(!WIFEXITED(status) || !WEXITSTATUS(status)) ) {
/* Report unexpected child status */
}
} else {
/*... Initialize env as a sanitized copy of environ ...*/
if (execve("/usr/bin/any_exe", args, env) == -1) {
perror("Error executing any_exe");
_exit(127);
}
}
This compliant solution is significantly different from the equivalent noncompliant code example. First, input is incorporated into the args array and passed as an argument to execve(). This eliminates any concerns about buffer overflow or string truncation while forming the command string. Second, this compliant solution must fork a new process before executing "/usr/bin/any_exe" in the child process. Although this method is more complicated than calling system(), the added security is worth the additional effort.
The exit status of 127 is the value set by the shell when a command is not found, and POSIX recommends that applications should do the same. XCU, Section 2.8.2 [Open Group 2004], says:
If a command is not found, the exit status shall be 127. If the command name is found, but it is not an executable utility, the exit status shall be 126. Applications that invoke utilities without using the shell should use these exit status values to report similar errors.
Noncompliant Code Example (POSIX)
This noncompliant code invokes the C system() function to remove the .config file in the user's home directory.
system("rm ~/.config");
If the vulnerable program has superuser privileges, an attacker can manipulate the value of HOME so that this program can remove any file named .config anywhere on the system.
Compliant Solution (POSIX)
One way to eliminate a system() call that executes an external program to perform a function required by the program is to implement the functionality directly in the program, preferably with existing library calls. For example, one way to remove a file without using system() is to use the POSIX unlink() function [Open Group 2004].
const char *file_format = "%s/foo";
const size_t len;
char *file;
struct passwd *pwd;
/* Get /etc/passwd entry for current user */
pwd = getpwuid(getuid());
if (pwd == NULL) {
/* Handle error */
return 1;
}
/* Build full path name home dir from pw entry */
len = strlen(pwd->pw_dir) + strlen(file_format);
file = (char *)malloc(len+1);
snprintf(file, len, file_format, pwd->pw_dir);
if (unlink(file) != 0) {
/* Handle error in unlink */
}
free(file);
file = NULL;
Be careful using unlink(), particularly when running with elevated privileges, because it may be susceptible to file-related race conditions. (See FIO01-C. Be careful using functions that use file names for identification.)
Risk Assessments
If the command string passed to system(), popen(), or other function that invokes a command processor is not fully sanitized, the risk of exploitation is high. In the worst case scenario, an attacker can execute arbitrary shellcode on the compromised machine with the privileges of the vulnerable process.
Use of the system() function can result in exploitable vulnerabilities:
- When passing an unsanitized or improperly sanitized command string originating from a tainted source
- If a command is specified without a path name and the command processor path name resolution mechanism is accessible to an attacker
- If a relative path to an executable is specified and control over the current working directory is accessible to an attacker
- If the specified executable program can be spoofed by an attacker
Although exceptions to this rule are necessary, they can be identified only on a case-by-case basis during a code review and are consequently outside the scope of this rule.
Recommendation | Severity | Likelihood | Remediation Cost | Priority | Level |
|---|---|---|---|---|---|
ENV04-C | high | probable | medium | P12 | L1 |
Automated Detection
Tool | Version | Checker | Description |
|---|---|---|---|
| Compass/ROSE | |||
| 2025.2 | SV.CODE_INJECTION.SHELL_EXEC | ||
| 9.7.1 | 588 S | Fully implemented | |
| PRQA QA-C | Unable to render {include} The included page could not be found. | Warncall -wc system | Partially implemented |
Related Vulnerabilities
Search for vulnerabilities resulting from the violation of this rule on the CERT website.
Related Guidelines
| CERT C++ Secure Coding Standard | ENV04-CPP. Do not call system() if you do not need a command processor |
| CERT Oracle Secure Coding Standard for Java | IDS07-J. Do not pass untrusted, unsanitized data to the Runtime.exec() method |
| ISO/IEC TR 24772:2013 | Unquoted Search Path or Element [XZQ] |
| ISO/IEC TR 17961 (Draft) | Calling system [syscall] |
| MITRE CWE | CWE-78, Failure to sanitize data into an OS command (aka "OS command injection") CWE-88, Argument injection or modification |
Bibliography
| [Open Group 2004] | XCU Section 2.8.2, "Exit Status for Commands"environ, execl, execv, execle, execve, execlp, execvp—Execute a Filepopen()unlink() |
| [Wheeler 2004] |