CWE-114: Process Control - C
Overview
In C, CWE-114 vulnerabilities occur when loading shared libraries or executing processes without proper validation. On Unix/Linux, this involves dlopen(), dlsym(), and exec*() functions. On Windows, LoadLibrary() and CreateProcess() are vulnerable. Attackers exploit weak library loading through DLL hijacking, LD_PRELOAD attacks, and path manipulation.
Primary Defence: Use absolute paths for library loading with dlopen() and LoadLibrary(), validate library names against allowlists, verify library signatures when possible, and use execve() with explicit argument arrays and sanitized environments to prevent command injection and path manipulation attacks.
Common Vulnerable Patterns
Using dlopen() with relative path (Unix/Linux)
#include <dlfcn.h>
#include <stdio.h>
// VULNERABLE - Searches LD_LIBRARY_PATH, can be hijacked
void load_plugin(const char *plugin_name) {
char library_path[256];
// User controls plugin_name - attacker can manipulate search path
snprintf(library_path, sizeof(library_path), "lib%s.so", plugin_name);
// dlopen() searches current dir, LD_LIBRARY_PATH, system paths
// Attacker can place malicious library in search path
void *handle = dlopen(library_path, RTLD_LAZY);
if (!handle) {
fprintf(stderr, "dlopen failed: %s\n", dlerror());
return;
}
// Execute code from potentially malicious library
}
// Attack: Set LD_LIBRARY_PATH=/tmp/evil or place evil library in current dir
Why this is vulnerable: Using relative paths with dlopen() allows attackers to control which library gets loaded by manipulating LD_LIBRARY_PATH, placing malicious libraries in the current directory, or exploiting the library search order to execute arbitrary code.
LoadLibrary() without absolute path (Windows)
#include <windows.h>
// VULNERABLE - Searches current directory first (DLL hijacking)
HMODULE load_library_unsafe(const char *dll_name) {
// LoadLibrary searches:
// 1. Application directory
// 2. Current directory ← DANGEROUS
// 3. System32
// 4. Windows directory
// 5. PATH directories
// Attacker places malicious DLL in current directory
HMODULE hModule = LoadLibrary(dll_name);
if (hModule == NULL) {
fprintf(stderr, "LoadLibrary failed: %lu\n", GetLastError());
}
return hModule;
}
// Attack: Place evil.dll in current directory, gets loaded instead of system DLL
Why this is vulnerable: LoadLibrary searches the current directory before system directories, allowing DLL hijacking attacks where attackers place malicious DLLs with legitimate names in user-writable locations to achieve code execution.
system() with user-controlled input
#include <stdlib.h>
#include <stdio.h>
// VULNERABLE - Command injection
void convert_image(const char *input_file) {
char command[512];
// User controls input_file - can inject shell commands
snprintf(command, sizeof(command), "convert %s output.png", input_file);
// Executes via /bin/sh - subject to shell interpretation
system(command);
}
// Attack: input_file = "input.jpg; rm -rf /"
// Executes: convert input.jpg; rm -rf / output.png
Why this is vulnerable: The system() function executes commands through a shell, allowing attackers to inject shell metacharacters (;, |, &, etc.) and execute arbitrary commands by manipulating the input string.
execve() with unsanitized PATH
#include <unistd.h>
#include <stdio.h>
// VULNERABLE - Uses PATH search, can execute wrong binary
void run_converter(const char *input_file) {
char *args[] = {"convert", (char *)input_file, "output.png", NULL};
// execlp() searches PATH for "convert"
// Attacker can manipulate PATH to execute malicious binary
execvp("convert", args);
perror("execvp failed");
}
// Attack: Set PATH=/tmp/evil:$PATH with malicious "convert" binary
Why this is vulnerable: Using execvp() searches the PATH environment variable for the executable, allowing attackers who can manipulate PATH to execute malicious binaries instead of the intended program.
Secure Patterns
dlopen() with absolute path and validation (Unix/Linux)
#include <dlfcn.h>
#include <stdio.h>
#include <string.h>
#include <limits.h>
#include <stdlib.h>
#include <sys/stat.h>
#define LIBRARY_DIR "/opt/app/lib"
#define MAX_LIBRARIES 3
static const char *allowed_libraries[] = {
"libcrypto.so",
"libssl.so",
"libcustom.so"
};
void *load_library_secure(const char *library_name) {
// Validate library is in allowlist
int found = 0;
for (int i = 0; i < MAX_LIBRARIES; i++) {
if (strcmp(library_name, allowed_libraries[i]) == 0) {
found = 1;
break;
}
}
if (!found) {
fprintf(stderr, "Library not in allowlist: %s\n", library_name);
return NULL;
}
// Construct absolute path
char absolute_path[PATH_MAX];
if (snprintf(absolute_path, sizeof(absolute_path),
"%s/%s", LIBRARY_DIR, library_name) >= sizeof(absolute_path)) {
fprintf(stderr, "Path too long\n");
return NULL;
}
// Resolve to canonical path (eliminates .., symlinks)
char canonical_path[PATH_MAX];
if (realpath(absolute_path, canonical_path) == NULL) {
perror("realpath failed");
return NULL;
}
// Verify path hasn't escaped library directory
if (strncmp(canonical_path, LIBRARY_DIR, strlen(LIBRARY_DIR)) != 0) {
fprintf(stderr, "Path traversal attempt detected\n");
return NULL;
}
// Verify file exists and is regular file
struct stat st;
if (stat(canonical_path, &st) != 0) {
perror("stat failed");
return NULL;
}
if (!S_ISREG(st.st_mode)) {
fprintf(stderr, "Not a regular file\n");
return NULL;
}
// Load with absolute path - bypasses LD_LIBRARY_PATH
void *handle = dlopen(canonical_path, RTLD_NOW | RTLD_LOCAL);
if (!handle) {
fprintf(stderr, "dlopen failed: %s\n", dlerror());
return NULL;
}
return handle;
}
Why this works:
- Absolute paths bypass
LD_LIBRARY_PATHand current-directory search. - Allowlists ensure only approved libraries are loadable.
realpath()collapses..and symlinks to prevent traversal.- Canonical path prefix checks prevent directory escape.
- File type validation blocks non-regular files.
LoadLibraryEx() with LOAD_LIBRARY_SEARCH_SYSTEM32 (Windows)
#include <windows.h>
#include <stdio.h>
#define MAX_PATH_LENGTH 260
HMODULE load_library_secure(const TCHAR *dll_name) {
// Load from System32 only (no current-directory search)
HMODULE hModule = LoadLibraryEx(
dll_name,
NULL,
LOAD_LIBRARY_SEARCH_SYSTEM32
);
if (hModule == NULL) {
fprintf(stderr, "LoadLibraryEx failed: %lu\n", GetLastError());
return NULL;
}
return hModule;
}
// Alternative: Load from application directory only
HMODULE load_app_library_secure(const TCHAR *dll_name) {
TCHAR app_path[MAX_PATH_LENGTH];
TCHAR full_path[MAX_PATH_LENGTH];
// Get application directory
if (GetModuleFileName(NULL, app_path, MAX_PATH_LENGTH) == 0) {
fprintf(stderr, "GetModuleFileName failed\n");
return NULL;
}
// Remove executable name, keep directory
TCHAR *last_slash = _tcsrchr(app_path, TEXT('\\'));
if (last_slash) {
*last_slash = TEXT('\0');
}
// Construct full path
HRESULT hr = StringCchPrintf(full_path, MAX_PATH_LENGTH,
TEXT("%s\\%s"), app_path, dll_name);
if (FAILED(hr)) {
return NULL;
}
// Load from application directory only
return LoadLibraryEx(
full_path,
NULL,
LOAD_LIBRARY_SEARCH_APPLICATION_DIR
);
}
Why this works:
- Search flags restrict loading to trusted directories only.
- The current-directory search is eliminated, blocking DLL hijacking.
- Absolute paths avoid PATH-based resolution.
GetSystemDirectory()/GetModuleFileName()anchor paths to trusted locations.
execve() with absolute path and cleared environment
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <sys/wait.h>
#define ALLOWED_BINARY_DIR "/usr/bin"
int execute_command_secure(const char *input_file, const char *output_file) {
// Validate input file path
char input_canonical[PATH_MAX];
if (realpath(input_file, input_canonical) == NULL) {
perror("realpath failed for input");
return -1;
}
// Verify input is within allowed directory
const char *allowed_input_dir = "/opt/app/uploads";
if (strncmp(input_canonical, allowed_input_dir,
strlen(allowed_input_dir)) != 0) {
fprintf(stderr, "Input file outside allowed directory\n");
return -1;
}
// Use absolute path to binary - bypasses PATH
const char *binary_path = "/usr/bin/convert";
// Arguments as array (no shell interpretation)
char *args[] = {
"convert",
input_canonical,
(char *)output_file,
NULL
};
// Create minimal safe environment (no LD_PRELOAD, no LD_LIBRARY_PATH)
char *env[] = {
"PATH=/usr/bin:/bin",
"HOME=/tmp",
"LANG=C",
NULL
};
pid_t pid = fork();
if (pid == -1) {
perror("fork failed");
return -1;
}
if (pid == 0) {
// Child process
execve(binary_path, args, env);
// execve only returns on error
perror("execve failed");
_exit(1);
}
// Parent process - wait for child
int status;
if (waitpid(pid, &status, 0) == -1) {
perror("waitpid failed");
return -1;
}
if (WIFEXITED(status)) {
return WEXITSTATUS(status);
}
return -1;
}
Why this works:
- Absolute binary paths bypass
PATHsearch hijacking. - A clean environment removes
LD_PRELOAD/LD_LIBRARY_PATHabuse. realpath()validation blocks path traversal.execve()avoids shell parsing and metacharacter injection.
Secure library loading with signature verification (Unix)
#include <dlfcn.h>
#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
#include <openssl/sha.h>
#include <openssl/crypto.h>
#include <limits.h>
#define LIBRARY_DIR "/opt/app/lib"
typedef struct {
const char *name;
const unsigned char *sha256_hash;
} trusted_library_t;
static const unsigned char libcrypto_hash[] = {
0xab, 0xcd, 0xef, /* ... 32 bytes total ... */
};
static const trusted_library_t trusted_libraries[] = {
{"libcrypto.so", libcrypto_hash},
{NULL, NULL}
};
int verify_library_hash(const char *path, const unsigned char *expected_hash) {
FILE *f = fopen(path, "rb");
if (!f) {
perror("fopen");
return 0;
}
EVP_MD_CTX *ctx = EVP_MD_CTX_new();
const EVP_MD *md = EVP_sha256();
unsigned char hash[EVP_MAX_MD_SIZE];
unsigned int hash_len;
EVP_DigestInit_ex(ctx, md, NULL);
unsigned char buffer[8192];
size_t bytes;
while ((bytes = fread(buffer, 1, sizeof(buffer), f)) > 0) {
EVP_DigestUpdate(ctx, buffer, bytes);
}
EVP_DigestFinal_ex(ctx, hash, &hash_len);
EVP_MD_CTX_free(ctx);
fclose(f);
// Compare hashes (constant-time to prevent timing attacks)
return CRYPTO_memcmp(hash, expected_hash, SHA256_DIGEST_LENGTH) == 0;
}
void *load_trusted_library(const char *library_name) {
// Find in trusted list
const trusted_library_t *lib = NULL;
for (int i = 0; trusted_libraries[i].name != NULL; i++) {
if (strcmp(library_name, trusted_libraries[i].name) == 0) {
lib = &trusted_libraries[i];
break;
}
}
if (!lib) {
fprintf(stderr, "Library not in trusted list\n");
return NULL;
}
// Construct absolute path
char path[PATH_MAX];
snprintf(path, sizeof(path), "%s/%s", LIBRARY_DIR, library_name);
// Verify hash before loading
if (!verify_library_hash(path, lib->sha256_hash)) {
fprintf(stderr, "Library hash verification failed - possible tampering\n");
return NULL;
}
// Load verified library
return dlopen(path, RTLD_NOW | RTLD_LOCAL);
}
Why this works:
- SHA-256 verification detects tampering or replacement.
- Attackers cannot swap libraries without failing the hash check.
- Absolute paths and allowlists reduce the load surface.
- Defense-in-depth requires name, path, and hash to match.
Verification
After implementing secure library loading with absolute paths and allowlist validation, verify the fix through multiple approaches:
- Manual testing: Attempt to load libraries from unauthorized paths and verify they're rejected
- Code review: Confirm all library loading uses absolute paths with no relative paths or environment-dependent searches
- Static analysis: Use security scanners to verify no process control vulnerabilities exist
- Regression testing: Ensure legitimate library loading and application functionality continue to work correctly
- Edge case validation: Test with path traversal attempts (
../, symlinks) to verify proper validation - Environment isolation: Verify
LD_LIBRARY_PATHandLD_PRELOADcannot influence library loading - Hash verification: If implemented, confirm cryptographic signatures are validated before loading
- Process inspection: Check
/proc/<pid>/maps(Linux) or equivalent to verify libraries loaded from expected absolute paths only - Rescan: Run the security scanner again to confirm the finding is resolved