Skip to content

CWE-114: Process Control

Overview

Process control weaknesses occur when untrusted input decides which program a process runs, which library it loads, or what happens to a process that is already running (start, stop, kill, priority, resource limits). That covers insecure dynamic library loading (DLL hijacking, LD_PRELOAD attacks), command injection through process execution, and process control operations nobody authorized.

Relationship to Other CWEs

CWE-114 is a Class-level weakness, and MITRE's own mapping guidance discourages using it directly on a finding: "it does not have Base-level children" and "combines multiple weaknesses that are related to the same behavior (process invocation)." Its ChildOf relationships are CWE-73 (External Control of File Name or Path) and CWE-20 (Improper Input Validation). Before mapping a finding to CWE-114, check whether it fits a more specific weakness:

  • CWE-114 (this page) - the broad case: untrusted input decides which program runs, which library loads, or what happens to a process already running.
  • CWE-426 (Untrusted Search Path) - the loader searches a path an attacker can influence (LD_LIBRARY_PATH, current-directory DLL search order)
  • CWE-427 (Uncontrolled Search Path Element) - the search path itself is fixed, but one of its elements points to an attacker-writable location
  • CWE-73 - the attacker can specify the exact library/executable path or filename directly

The library-loading guidance below is squarely in CWE-426/427 territory; use CWE-114 for the broader process-lifecycle case (kill/start/stop/priority) that those two don't cover.

CWE-114 and CWE-78 address different aspects of process security.

CWE-114 (Process Control) - untrusted data controls which process or library runs:

  • Focus: Controlling the executable path, library name, or process lifecycle (start/stop/kill)
  • Examples: System.loadLibrary(userInput), dlopen(userPath), LoadLibrary(userDll), os.kill(userPid)
  • Prevention: Absolute paths, allowlist validation, path traversal prevention, signature verification
  • No shell involved: Direct API calls to load libraries or control processes

CWE-78 (OS Command Injection) - untrusted data is interpreted by a shell as commands:

  • Focus: Shell metacharacters in arguments (;, |, &, $(), backticks)
  • Examples: system("convert " + userInput), exec("grep " + userPattern)
  • Prevention: Avoid shell execution, use argument arrays, input sanitization
  • Requires shell: Vulnerability exists because shell interprets special characters

Overlap scenarios (both apply):

  • system(userInput) in C, exec() in Node.js, subprocess.run(cmd, shell=True) in Python - each spawns a shell, so untrusted input controls both which program runs and what else runs alongside it
  • Any exec-family call whose command string is /bin/sh -c ... or cmd.exe /c ... - the wrapper is the shell, regardless of which API delivered it

Not an overlap, despite appearances: Java's Runtime.exec(String) does not spawn a shell. It splits the string on whitespace and execs the first token, so shell metacharacters in it are inert - but the attacker still controls the argument vector of the named program, which is CWE-114 on its own. See the Java page for what that buys an attacker.

Distinct scenarios (CWE-114 only, no shell):

  • Library loading: System.load(), dlopen(), LoadLibrary() - no shell interpretation
  • Process lifecycle: os.kill(), Process.kill() - no command execution
  • Direct execution: execve(), ProcessBuilder without shell - prevents CWE-78 but still needs CWE-114 protection

The remediation steps below close both weaknesses:

  1. Direct execution APIs without a shell (prevents CWE-78)
  2. Validated executable paths and allowlists (prevents CWE-114)
  3. Argument arrays instead of string concatenation (prevents both)

OWASP Classification

A05:2025 - Injection

Risk

High: An attacker who controls which library or executable gets loaded runs code of their choosing inside the target process, with that process's privileges. Where the weakness is over the process lifecycle instead, the cost is availability: critical processes terminated, processes spawned until resources run out, or priorities changed to starve the work that matters.

Remediation Steps

Core Principle: Only load components from trusted, integrity-checked locations; control library/module search paths and provenance.

Identify the Process Control Vulnerability

Work out how untrusted data reaches the process operation:

  • Source: where the untrusted value enters - HTTP parameters, database rows, external files, network requests
  • Sink: the process control call it reaches (os.kill(), Process.kill(), taskkill, etc.)
  • Operation: what that call does - kill, start, stop, priority change
  • Missing controls: whether anything validates or authorizes the value between source and sink

Implement Strict Authorization Checks

Check authorization before every process control operation:

  • Authenticate the user before allowing any process control
  • Restrict process control to specific roles, such as admins or operators
  • Require additional authentication for sensitive operations (system processes, critical services)
  • Never accept PIDs, process names, or control commands directly from untrusted sources
  • Verify the caller owns the target process, or holds an admin role, before acting on it

Use Allowlists for Process Operations

Restrict which processes can be controlled:

  • Define which processes or services the application may manage, and load that list from configuration rather than code
  • Check process names or IDs against the allowlist before any operation
  • Never allow control of system processes (init, systemd, kernel threads, Windows services)
  • Map user input to internal identifiers: the user selects "job-123", and the application resolves that to a specific PID

Validate All Process Control Parameters

Validate process identifiers and commands before using them:

  • PIDs must be positive integers within the valid range
  • Process names must match the allowlist; reject path traversal attempts
  • Control commands must be one of a fixed set (start, stop, restart); reject anything else
  • Confirm the process exists before attempting control
  • Confirm the process belongs to the application or to an authorized user
  • When spawning processes, set memory, CPU, and time limits

Constrain Which Components Can Be Loaded

The lifecycle guidance above covers processes the application controls. The other half of this CWE is which library, module, plugin or executable gets loaded, and it needs a different sequence:

  • Remove the choice where you can: if the set of loadable components is fixed at build time, call the loader with a constant. An allowlist consulted at compile time cannot be bypassed at runtime, and most findings in this class are a parameter that never needed to exist.
  • Allowlist the name, then build the path: validate the request value against a list of permitted names before it is joined to anything. Once it is part of a path string, every later check is trying to undo it.
  • Resolve to an absolute path from a fixed base directory, so the platform's search order is never consulted - no PATH, no LD_LIBRARY_PATH, no current directory, no java.library.path.
  • Canonicalize, then check containment, then load - in that order. Compare against the base directory with its separator appended: a plain string-prefix test also accepts a sibling directory whose name merely starts the same way, which is the most common way this check is written wrong.
  • Verify the bytes if the directory is not already protected: a pinned hash or a verified signature, checked before the load rather than after it. A check that runs after loading reports on code that is already resident.
  • Prefer filesystem permissions where they are available: a component directory the application cannot write to, owned by the deployment process, gives you most of what pinning gives you without coupling every dependency upgrade to a code change.

Separate Command Injection from Argument Injection

Two different weaknesses hide behind "the input reached an exec call", and the second one survives the fix for the first:

  • Command injection needs a shell in the chain. Passing arguments as an array to execve, ProcessBuilder, ArgumentList or spawn(..., {shell: false}) closes it completely - nothing is left to parse ;, | or $().
  • Argument injection does not need a shell. An attacker-supplied value that begins with - is read as an option by the program you invoked, and the dangerous options are program-specific: convert -write, tar --to-command, curl -o, ssh -o ProxyCommand=, find -exec. Escaping does nothing about it.

Where the program supports --, put it before the positional arguments. Where it does not, resolve the value against a fixed base directory so the result can never begin with -, or reject a leading - outright.

Implement Logging and Monitoring

Detect and respond to suspicious process control activity:

  • Log every process control attempt with user context, timestamp, operation type, and target process
  • Alert on mass termination attempts, rapid process spawning, and privilege escalation
  • Notify the security team when authorization checks fail
  • Track process creation, termination, and control events
  • Rate-limit process control endpoints to slow automated abuse

Test with Malicious Process Control Attempts

Confirm the fix rejects unauthorized process control:

  • Attempt process control as a non-admin user; expect rejection
  • Send invalid PIDs: negative numbers, zero, non-existent PIDs, extremely large values
  • Attempt to kill a system process; expect the allowlist to block it
  • If the PID comes from a database, test the query for SQL injection
  • Attempt to kill many processes in quick succession; expect rate limiting to stop it
  • Confirm that authorized users can still control allowed processes

Common Vulnerable Patterns

  • Accepting user input for process kill/terminate operations
  • Using user-controlled PIDs without validation
  • Missing authorization checks on process control
  • Allowing control of arbitrary system processes
  • No rate limiting on process spawning

Language-Specific Guidance

Concrete APIs and worked examples are on the language pages:

  • C: Secure native library loading with dlopen() using absolute paths, LoadLibraryEx() on Windows, execve() for process execution, environment clearing, and LD_PRELOAD prevention
  • C#: DLL search-order hardening with SetDefaultDllDirectories()/AddDllDirectory(), Process.Start() with ArgumentList instead of a command string, hash-pinned assembly loading, and why the strong-name public key token is not a signature check on .NET 5+
  • Java: Secure library loading with System.load() using absolute paths, ProcessBuilder with argument arrays, path validation, allowlist enforcement, SHA-256 pinning, and what Runtime.exec(String) actually does with its argument
  • JavaScript/Node.js: Process control security in Node.js using child_process, PM2, cluster module, and container orchestration
  • Python: Process control security using os.kill(), subprocess, signal module, and frameworks like psutil, Celery, and Docker

Additional Resources