Description
system(), popen(), and _popen() pass a string to a command interpreter, as does C++'s std::system() from <cstdlib>. If the string includes untrusted input, shell metacharacters, command separators, redirections, or additional arguments may be interpreted as code. wordexp() also performs shell word expansion and can execute command substitutions unless WRDE_NOCMD is specified.
Escaping shell characters alone cannot safely handle every shell and target program's syntax. Even when shell commands are blocked, argument injection may remain if the target program treats untrusted values as options. A helper name such as shell_escape() or sanitize_command_arg() does not establish safety.
Potential impact
- Arbitrary operating system commands may run with the application's privileges.
- Consequences can include file deletion or modification, sensitive data exposure, internal system access, and service disruption.
- A highly privileged process may expose the entire system to compromise.
Remediation
Prefer library APIs that provide the required function without an external command. If a separate process is necessary:
- Fix the executable path in code and use
execve(), an appropriateposix_spawn()function, or the platform's process API. - Keep the executable and arguments separate instead of assembling a command string.
- Validate each argument against an allow-list for the target program's data format. Check option injection risks, including leading
-and the handling of--. - Supply only necessary environment variables with trusted values and handle errors.
- If
wordexp()is unavoidable, includeWRDE_NOCMDand check its return value. It rejects requested command substitution withWRDE_CMDSUB; it does not make other forms of word expansion generally safe.
Examples
C
Before
#include <stdlib.h>
int main(int argc, char **argv) {
if (argc != 2) {
return 2;
}
return system(argv[1]);
}
After
#include <ctype.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
static int valid_label(const char *value) {
size_t length = strlen(value);
if (length == 0 || length > 64) {
return 0;
}
for (size_t i = 0; i < length; ++i) {
unsigned char ch = (unsigned char)value[i];
if (!isalnum(ch) && ch != '.' && ch != '_' && ch != '-') {
return 0;
}
}
return 1;
}
int main(int argc, char **argv) {
if (argc != 2 || !valid_label(argv[1])) {
return 2;
}
char *const args[] = {"printf", "Archive: %s\n", argv[1], NULL};
execv("/usr/bin/printf", args);
perror("execv");
return 1;
}
Explanation:
- Before: The entire
argv[1]value becomes a shell command. Adding quotes does not make all shell syntax safe. - After: The executable and format string are fixed, and the user value is a separate argument. Allow-list validation also restricts the value to its intended business format.
If wordexp() is needed, explicitly prohibit command substitution and handle every error.
#include <wordexp.h>
int expand_without_commands(const char *input, wordexp_t *words) {
int result = wordexp(input, words, WRDE_NOCMD | WRDE_UNDEF);
if (result != 0) {
if (result == WRDE_NOSPACE) {
wordfree(words);
}
return -1;
}
return 0;
}
On success, the caller must release the result with wordfree() after use. Partially allocated memory must also be released on WRDE_NOSPACE.
C++
Before
#include <cstdlib>
int main(int argc, char **argv) {
if (argc != 2) {
return 2;
}
return std::system(argv[1]);
}
After
#include <cctype>
#include <cstdio>
#include <cstring>
#include <unistd.h>
static bool valid_label(const char *value) {
std::size_t length = std::strlen(value);
if (length == 0 || length > 64) {
return false;
}
for (std::size_t i = 0; i < length; ++i) {
unsigned char ch = static_cast<unsigned char>(value[i]);
if (!std::isalnum(ch) && ch != '.' && ch != '_' && ch != '-') {
return false;
}
}
return true;
}
int main(int argc, char **argv) {
if (argc != 2 || !valid_label(argv[1])) {
return 2;
}
char program[] = "/usr/bin/printf";
char format[] = "Archive: %s\n";
char *const args[] = {program, format, argv[1], nullptr};
execv(program, args);
std::perror("execv");
return 1;
}
Call a fixed executable directly instead of using std::system(), and pass the user value as a separate argument. This example is valid in a C++23 and POSIX.1-2024 environment; the allow-list also restricts the value to its business format.
References
- POSIX.1-2024
system() - POSIX.1-2024 Shell Command Language
- POSIX.1-2024
wordexp() - POSIX.1-2024
execfunctions - POSIX.1-2024
posix_spawn() - Microsoft
_popen,_wpopen - Microsoft C++ Standard Library
<cstdlib> - Microsoft
system,_wsystem - ISO/IEC 14882:2024 Programming languages — C++
- OWASP OS Command Injection Defense Cheat Sheet
- OWASP ASVS 5.0.0 V1.2.5
- SEI CERT C ENV33-C: Do not call
system() - CWE-78: Improper Neutralization of Special Elements used in an OS Command
- OWASP Top 10:2025 A05 Injection
- OWASP Top 10:2021 A03 Injection