flawopen.com/command-injection/C-cpp
OS Command Injection in C/C++ (posix_spawn)
Formatting input into a string for system() or popen(), even inside hand-written single quotes, or passing a value that starts with a dash through posix_spawnp() lets a request run code on your C or C++ service (CWE-78). Why posix_spawn() with an argv array is the fix and how to bound the child with timeout(1).
Think of a print shop where system() and popen() hand the whole job description to /bin/sh -c; a semicolon in a formatted string becomes another instruction. Even putting user text inside hand-written quotes is unsafe because an apostrophe can close them. posix_spawn() with an argv array skips the shell, so each value stays one argument. But posix_spawnp() and execvp() search PATH, and a leading dash can still select an option in the target program. Use an absolute executable path, validate arguments, mark the end of options, and bound the child process. For a Git revision, git rev-parse --verify --end-of-options "$revision" ends option parsing; -- has separate, command-specific operand or path semantics.
Core Concepts & Subsystem Terms
system() and popen()- Both run
/bin/sh -cwith the string you pass, so;,|,&&,$( )and backticks in formatted input start extra commands. CERT rule ENV33-C says not to callsystem()at all. Hand-quoting with '%s'- Wrapping
%sin single quotes looks like escaping, but a'in the input closes the quote:a'; id; echo 'turns one argument into three commands. The shell's quoting rules are too subtle to reproduce by hand. posix_spawn() with an argv array- Starts the program at an absolute path with each array element as one argument and no shell, so metacharacters are plain bytes.
posix_spawnp()andexecvp()also searchPATH, so prefer the non-pforms with absolute paths. Argument injection and Git option boundaries- Separate argv entries prevent shell parsing, but the target program still parses its own options. For an untrusted Git revision, use
git rev-parse --verify --end-of-options "$revision": this ends option parsing.--has command-specific operand or path semantics; ingit log, it separates revisions from paths. These markers are not interchangeable. Validate that the value is a revision before use. timeout(1) and waitpid()waitpid()blocks until the child exits, however long that takes. Launching the program through/usr/bin/timeout 5gives it a hard limit without extra signal-handling code.
Framework mappings
- CWE reference for the weakness described in this guide: CWE-78
- OWASP Top 10:2025 — A05:2025 Injection
Step-by-Step Attack Flow
Request value reaches the helper
A CGI program or an embedded web server's handler copies a query parameter into host, name or ref and calls a helper that starts a process.
The value becomes part of a command
snprintf writes host into a system() string, name breaks out of the single quotes in a popen() string, or a value that looks like an option reaches git. The benign test value 127.0.0.1; id illustrates the shell case.
The shell or the program interprets it
sh runs ping and then id, or runs the command after the closing quote; Git treats --output=... as an option.
The attacker runs code on the device or server
The process runs as the service user, often root on embedded devices: it can read configuration and keys, reach internal services and write files.
Source Code: Flaw vs. Secure Implementation
/* tools.c: each function lets request data change what runs */
#include <spawn.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
extern char **environ;
/* system() runs the formatted string through sh -c, ; and all */
int ping_host(const char *host)
{
char cmd[512];
snprintf(cmd, sizeof cmd, "ping -c 1 %s", host);
return system(cmd);
}
/* a ' in name closes the quote; popen() also uses sh -c */
FILE *make_thumbnail(const char *name)
{
char cmd[512];
snprintf(cmd, sizeof cmd,
"convert '/srv/uploads/%s' -resize 200x200 /srv/thumbs/out.png", name);
return popen(cmd, "r");
}
/* no shell, but a ref starting with - is read as a Git option */
int commit_log(const char *ref)
{
char *const argv[] = {"git", "log", (char *)ref, NULL};
pid_t pid;
int status;
if (posix_spawnp(&pid, "git", NULL, NULL, argv, environ) != 0)
return -1;
waitpid(pid, &status, 0);
return status;
}
/* tools.c: posix_spawn, argv arrays, allow-lists and time limits */
#include <ctype.h>
#include <spawn.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
extern char **environ;
/* absolute path, one argv element per argument, no shell */
static int run(char *const argv[])
{
pid_t pid;
int status;
if (posix_spawn(&pid, argv[0], NULL, NULL, argv, environ) != 0)
return -1;
if (waitpid(pid, &status, 0) < 0)
return -1;
return WIFEXITED(status) ? WEXITSTATUS(status) : -1;
}
static bool allowed(const char *s, size_t max_len, const char *extra)
{
size_t len = strlen(s);
if (len == 0 || len > max_len || !isalnum((unsigned char)s[0]))
return false;
for (size_t i = 0; i < len; i++) {
if (!isalnum((unsigned char)s[i]) && strchr(extra, s[i]) == NULL)
return false;
}
return true;
}
/* ping runs directly under a 5-second timeout */
int ping_host(const char *host)
{
if (!allowed(host, 253, ".-"))
return -1;
char *const argv[] = {"/usr/bin/timeout", "5", "/bin/ping", "-c", "1", (char *)host, NULL};
return run(argv);
}
/* no quoting needed: the path is one argv element */
int make_thumbnail(const char *name)
{
if (!allowed(name, 64, "._-"))
return -1;
char path[128];
snprintf(path, sizeof path, "/srv/uploads/%s", name);
char *const argv[] = {"/usr/bin/timeout", "10", "/usr/bin/convert", path,
"-resize", "200x200", "/srv/thumbs/out.png", NULL};
return run(argv);
}
/* --end-of-options makes Git treat ref as a revision */
int commit_log(const char *ref)
{
if (!allowed(ref, 100, "._/-"))
return -1;
char *const argv[] = {"/usr/bin/timeout", "5", "/usr/bin/git", "log",
"--end-of-options", (char *)ref, "--", NULL};
return run(argv);
}
Engineering & System Hardening Checklist
- ✓Enable clang-tidy
cert-env33-cto flagsystem(), and add a CI grep rule that fails on newpopen(,execl("/bin/sh"andsnprintfcalls that build command lines. - ✓Start programs with
posix_spawn()orexecv()using an absolute path and an argv array; never format a command line for the shell. - ✓Reject option-like values by default. For an untrusted Git revision, call
git rev-parse --verify --end-of-options "$revision"; use--only with the operand or path meaning documented by that exact command. - ✓Bound every child with
/usr/bin/timeoutor awaitpid()loop pluskill(), and check the exit status instead of ignoring it. - ✓Run the service as an unprivileged user, never as root, and pass a minimal environment where the program does not need the parent's.
Sources
- CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')
- POSIX — posix_spawn
- SEI CERT C — ENV33-C. Do not call system()
- clang-tidy — cert-env33-c
- GNU coreutils — timeout
- OWASP OS Command Injection Defense Cheat Sheet
- POSIX Utility Syntax Guidelines
- Git rev-parse — --end-of-options
- Git — Command-Line Interface Conventions
- Git — git log