FlawOpen

flawopen.com/command-injection/C-cpp

● CWE-78 · Critical
FlawOpen Security Research

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).

💡 Plain English Explainer (ELI5)

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 -c with the string you pass, so ;, |, &&, $( ) and backticks in formatted input start extra commands. CERT rule ENV33-C says not to call system() at all.
Hand-quoting with '%s'
Wrapping %s in 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() and execvp() also search PATH, so prefer the non-p forms 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; in git 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 5 gives it a hard limit without extra signal-handling code.

Framework mappings

Step-by-Step Attack Flow

Step 1

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.

Step 2

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.

Step 3

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.

Step 4

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

UNPATCHED FLAW
/* 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;
}
HARDENED SECURE PATCH
/* 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

Sources