The C Standard
system()
function executes a specified command by invoking an implementation-defined
command processor, such as a UNIX shell or
CMD.EXE
in Microsoft Windows. The POSIX popen()
and Windows
_popen()
functions also invoke a command processor but create 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 [IEEE
Std 1003.1:2013].
Use of the system() function can result in exploitable vulnerabilities, in the worst case allowing execution of arbitrary system commands. Situations in which calls to system() have high risk include the following:
- 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
Do not
invoke a command processor via system()
or
equivalent functions to execute a command.
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.
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
enum
{ BUFFERSIZE = 512 };
void
func(
const
char
*input) {
char
cmdbuf[BUFFERSIZE];
int
len_wanted = snprintf(cmdbuf,
BUFFERSIZE,
"any_cmd '%s'"
, input);
if
(len_wanted >= BUFFERSIZE) {
/* Handle error */
}
else
if
(len_wanted < 0) {
/* Handle error */
}
else
if
(
system
(cmdbuf) == -1) {
/* Handle error */
}
}
|
If this code is compiled and run with elevated 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.
This noncompliant code 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 does not use a full shell interpreter, so it is
not vulnerable to command-injection attacks, such as the one
illustrated in the noncompliant code example.
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 forward
slash character (
/
). As a result, they should be used without a forward 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.
Additionally, precautions should be taken to ensure the external executable cannot be modified by an untrusted user, for example, by ensuring the executable is not writable by the user.
#include
<sys/types.h>
#include
<sys/wait.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
void
func(
char
*input) {
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) {
/* Handle error */
}
else
if
(pid != 0) {
while
((ret = waitpid(pid,
&status, 0)) == -1) {
if
(
errno
!= EINTR) {
/* Handle error */
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_cmd"
, args, env) == -1) {
/* Handle error */
_Exit(127);
}
}
}
|
This compliant solution is significantly different from the preceding
noncompliant code example. First,
input
is incorporated into the
args
array and passed as an argument to
execve()
, eliminating concerns about buffer overflow or string truncation
while forming the command string. Second, this compliant solution
forks a new process before executing
"/usr/bin/any_cmd"
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, of Standard for Information TechnologyPortable Operating System Interface (POSIX®), Base Specifications, Issue 7 [IEEE Std 1003.1:2013], 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.
Compliant Solution (Windows)
This compliant solution uses the Microsoft Windows CreateProcess()
API:
#include
<Windows.h>
void
func(
TCHAR
*input) {
STARTUPINFO si = { 0 };
PROCESS_INFORMATION pi;
si.cb =
sizeof
(si);
if
(!CreateProcess(TEXT(
"any_cmd.exe"
), input, NULL, NULL, FALSE,
0, 0, 0, &si, &pi)) {
/* Handle error */
}
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
}
|
This compliant solution relies on the
input
parameter being non-
const
. If it were
const
, the solution would need to create a copy of the parameter because
the
CreateProcess()
function can modify the command-line arguments to be passed into the
newly created process.
This solution creates the process such that the child process does not inherit any handles from the parent process, in compliance with WIN03-C. Understand HANDLE inheritance.
Noncompliant Code Example (POSIX)
This noncompliant code invokes the C
system()
function to remove the
.config
file in the user's home directory.
#include <stdlib.h>
void
func(
void
) {
system
(
"rm ~/.config"
);
}
|
If the vulnerable program has elevated privileges, an attacker can
manipulate the value of the
HOME
environment variable such that this program can remove any file named
.config
anywhere on the system.
Compliant Solution (POSIX)
An alternative to invoking the
system()
call to execute an external program to perform a required operation is
to implement the functionality directly in the program using existing
library calls. This compliant solution calls the POSIX
function to remove a file without invoking the
unlink()
system()
function [IEEE
Std 1003.1:2013]
#include <pwd.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
void
func(
void
) {
const
char
*file_format =
"%s/.config"
;
size_t
len;
char
*pathname;
struct
passwd *pwd;
/* Get /etc/passwd entry for
current user */
pwd = getpwuid(getuid());
if
(pwd == NULL) {
/* Handle error */
}
/* Build full path name home
dir from pw entry */
len =
strlen
(pwd->pw_dir) +
strlen
(file_format) + 1;
pathname = (
char
*)
malloc
(len);
if
(NULL == pathname) {
/* Handle error */
}
int
r = snprintf(pathname, len,
file_format, pwd->pw_dir);
if
(r < 0 || r >= len) {
/* Handle error */
}
if
(unlink(pathname) != 0) {
/* Handle error */
}
free
(pathname);
}
|
The
unlink()
function is not susceptible to a symlink attack where
the final component of
pathname
(the file name) is a symbolic link because
unlink()
will remove the symbolic link and not affect any file or directory
named by the contents of the symbolic link. (See FIO01-C.
Be careful using functions that use file names for identification.)
While this reduces the susceptibility of the
unlink()
function to symlink attacks, it does not eliminate it. The
unlink()
function is still susceptible if one of the directory names included
in the
pathname
is a symbolic link. This could cause the
unlink()
function to delete a similarly named file in a different directory.
Compliant Solution (Windows)
This compliant solution uses the Microsoft Windows SHGetKnownFolderPath()
API to get the
current user's My Documents folder, which is then combined with
the file name to create the path to the file to be
deleted. The file is then removed using the DeleteFile()
API.
#include
<Windows.h>
#include <ShlObj.h>
#include
<Shlwapi.h>
#if defined(_MSC_VER)
#pragma comment(lib,
"Shlwapi")
#endif
void
func(
void
) {
HRESULT
hr;
LPWSTR
path = 0;
WCHAR
full_path[MAX_PATH];
hr =
SHGetKnownFolderPath(&FOLDERID_Documents, 0, NULL,
&path);
if
(FAILED(hr)) {
/* Handle error */
}
if
(!PathCombineW(full_path, path,
L
".config"
)) {
/* Handle error */
}
CoTaskMemFree(path);
if
(!DeleteFileW(full_path)) {
/* Handle error */
}
}
|
Exceptions
ENV33-C-EX1: It is permissible to
call
system()
with a null pointer argument to determine the
presence of a command processor for the system.