CWE-114, Process Control, is a class level weakness. Executing commands or loading libraries from an untrusted source, or in an untrusted environment, can cause an application to run malicious commands and payloads on behalf of an attacker.
It affects anything that launches programs or loads native libraries: installers, build scripts, admin tools, plugin loaders, and applications that wrap a command line utility. MITRE lists it as not language specific.
How it happens
MITRE describes two forms.
- The attacker changes the command. Data from an untrusted source becomes part of a string representing a command, and the application runs it. By running the command, the application gives the attacker a capability they would not otherwise have.
- The attacker changes the environment. The command is fixed, but its meaning is not. If the attacker controls a search path, a working directory or the location of a library, the same command name resolves to something else.
# Form 1: the user influences which program runs
import subprocess
def convert(tool, path):
subprocess.run([tool, path]) # tool comes from a request parameter
# Form 2: relies on the search path to find the program
subprocess.run(["convert", path]) # which "convert" is found first?
Library loading follows the same logic. An application that loads a native library by bare name, or from a directory that other users can write to, will load whatever sits there.
What an attacker gains
MITRE lists confidentiality, integrity and availability impact through the execution of unauthorized code or commands. The code runs with the privileges of your application, so the damage grows with those privileges.
How to find it
MITRE says static analysis can find some instances. Search for process launching calls (exec, system, popen, Runtime.exec, subprocess, ProcessBuilder) and library loading calls (dlopen, LoadLibrary, System.loadLibrary). For each, answer: where does the program name come from, and where does the file get resolved from?
How to fix it
MITRE's mitigation concerns libraries and frameworks. Libraries that are loaded should be well understood and come from a trusted source, because the application can execute code in them, and native libraries often contain calls prone to buffer overflows or command injection. In practice:
- Validate that you need each native library at all. MITRE notes that it is hard to know what native libraries actually do, the chance of malicious code is high, and the chance of an honest mistake in C or C++ code is also high.
- Validate input to native calls for content and length to help prevent buffer overflows.
- Review the source if the library does not come from a trusted source, and build it from the reviewed source before use.
- Use absolute paths for programs and libraries, and a fixed allowlist of commands rather than a name from input.
ALLOWED_TOOLS = {"pdf": "/usr/bin/pdftotext", "img": "/usr/bin/convert"}
def convert(kind, path):
tool = ALLOWED_TOOLS[kind] # KeyError for anything unexpected
subprocess.run([tool, path], check=True)
Third party packages count as loaded code too. The free in-browser scan at gold.safeguard.sh/scan checks up to 100 dependencies with no login, and your file stays on your machine.
Quick check
What are the two forms of process control?
The attacker controls the command itself, or controls the environment that decides what the command means.