Sep 9, 2026·6 min read·45 visits
Unauthenticated remote command execution in Composer's Perforce VCS driver via malicious repository configurations or lock files.
A critical remote code execution vulnerability exists in the Composer PHP dependency manager due to improper neutralization of command parameters passed to the Perforce CLI client. Unauthenticated attackers can exploit this flaw via crafted package metadata in custom repositories or lock files, triggering arbitrary OS command execution when a user or automated CI/CD pipeline runs Composer commands.
Composer is the standard dependency manager for the PHP ecosystem, responsible for fetching, resolving, and installing package dependencies. To support diverse version control systems, Composer includes dedicated VCS drivers, including one for Perforce. This Perforce driver allows Composer to pull package sources directly from Perforce repositories.
The vulnerability, designated as CVE-2026-84361, is classified under CWE-78 (OS Command Injection). It arises because Composer's Perforce utility fails to sanitize or restrict the package-defined source URL before passing it to the local Perforce command-line client (p4). When a package configuration defines a Perforce source type, its URL is directly mapped to the P4PORT configuration parameter.
An attacker can abuse this behavior by crafting a repository configuration or a composer.lock file containing a malicious URL prefixed with rsh: or jsh:. If the system running Composer has the p4 CLI client installed, standard actions like composer install or composer update will automatically execute local shell commands embedded in the URL.
The root cause of CVE-2026-84361 lies in the underlying behavior of the Perforce CLI client (p4) in conjunction with Composer's unchecked parameter forwarding. To establish a connection with a Perforce server, the p4 client relies on the endpoint address specified via the P4PORT variable or the -p parameter. By default, this address represents a network location, such as a TCP or SSL socket.
However, Perforce natively supports alternative transport protocols for advanced scripting and tunneling configurations, specifically rsh: and jsh:. When the P4PORT parameter begins with these transport prefixes, the p4 executable does not connect over the network. Instead, it spawns the remainder of the parameter string as a local subprocess and pipes standard input/output through it to communicate.
In vulnerable versions of Composer, the Composer\\Util\\Perforce class processes package-defined URLs without validating their protocol scheme. When checking if a server exists or cloning a repository, Composer invokes p4 via a subprocess executor, passing the untrusted URL as the -p argument. Because this execution is a native feature of the p4 binary, traditional argument escaping or shell sanitization routines are ineffective. The command is not executed via a shell wrapper but is launched directly by the compiled p4 executable itself.
To understand the vulnerable path, we can examine how Composer handles Perforce URLs during repository verification. In vulnerable versions, the Perforce::checkServerExists() method executed a connection probe without verifying the format of the $url variable:
public static function checkServerExists(string $url, ProcessExecutor $processExecutor): bool
{
// Vulnerable: The $url parameter is passed directly to the p4 client via -p
return 0 === $processExecutor->execute(['p4', '-p', $url, 'info', '-s'], $ignoredOutput);
}The corresponding patch introduces a strict validation helper isValidPort() in src/Composer/Util/Perforce.php to filter out non-network endpoints before they reach the executable:
public static function isValidPort(string $url): bool
{
// Reject any transport keywords like rsh or jsh to prevent local subprocess execution
if (Preg::isMatch('{^\\s*+(?:rsh|jsh)\\s*+:}i', $url)) {
return false;
}
// Enforce strict whitelist of allowed tcp/ssl socket patterns
return Preg::isMatch('{^(?:(?:tcp|ssl)(?:4|6|46|64)?:)?(?:\\\\[[0-9a-f:.]++\\\\]|[a-z0-9._][a-z0-9._-]*+)(?::[a-z0-9._][a-z0-9._-]*+)?$}iD', $url);
}Additionally, this validation check is called during package loading inside ValidatingArrayLoader.php to catch unsafe URLs early and throw a SecurityException. The use of the /D modifier in the regular expression prevents newline injection attacks, ensuring complete remediation of the parameter parsing logic.
Exploitation of CVE-2026-84361 is straightforward if the victim's environment meets the prerequisites. The primary requirements are that the Perforce p4 command-line client is installed and accessible on the system's PATH, and the target executes a dependency resolution command on an untrusted configuration.
An attacker can trigger this vulnerability by publishing a malicious dependency within a custom package repository or by submitting a compromised composer.lock file to a repository. Below is an example of a malicious composer.json block that specifies a perforce repository type with an embedded command:
{
"type": "package",
"package": {
"name": "poc/perforce-source",
"version": "1.0.0",
"source": {
"type": "perforce",
"url": "rsh:touch /tmp/cve-2026-84361-composer-marker",
"reference": "//depot/main"
}
}
}When a developer or a continuous integration (CI) runner executes composer install or composer update --prefer-source, Composer parses this package definition. It attempts to verify the Perforce repository by executing p4 -p rsh:touch /tmp/... info -s. The p4 binary parses the rsh: transport scheme, spawns the sub-command, and triggers arbitrary command execution under the user's privilege level.
The impact of successful exploitation is critical, leading to full Remote Code Execution (RCE) on the host machine running Composer. In development environments, this grants the attacker access to local source code, environment variables, SSH keys, and local credentials.
In modern software engineering workflows, the impact on CI/CD pipelines is especially high. Automated systems frequently execute Composer commands as part of building, testing, or deploying applications. If a pipeline processes an untrusted pull request containing a malicious composer.lock file, the attacker can execute arbitrary commands on the build agent.
This execution can lead to compromised build artifacts, leaked cloud provider credentials, or secondary supply chain attacks if the build runner possesses write permissions to other repositories or container registries. The CVSS score of 7.7 reflects this risk, although the requirement for the p4 binary to be present on the host somewhat limits the default attack surface.
The primary and most effective remediation is upgrading Composer to a patched release. For legacy installations, the vulnerability is fixed in version 2.2.30. For modern environments, upgrading to 2.10.3 or later eliminates the flaw entirely.
If upgrading Composer is not immediately feasible, organizations should apply defensive mitigations to break the exploit chain. Since the exploit depends on the presence of the p4 command-line utility, removing the p4 binary from systems that do not use Perforce completely neutralizes the threat.
Additionally, team leads and security engineers should implement strict validation controls on package registries. Because Packagist.org does not permit Perforce source metadata, default public packages are safe. However, organizations hosting private registries should enforce schema validation to block the ingestion of any package containing a perforce source type with an unvalidated connection string.
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N| Product | Affected Versions | Fixed Version |
|---|---|---|
Composer Composer Project | >= 1.0, < 2.2.30 | 2.2.30 |
Composer Composer Project | >= 2.3.0, < 2.10.3 | 2.10.3 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-78 |
| Attack Vector | Network |
| CVSS Score | 7.7 |
| EPSS Score | 0.00409 |
| Impact | Remote Code Execution (RCE) |
| Exploit Status | Proof-of-Concept (PoC) available |
| KEV Status | Not listed |
The software constructs an OS command using externally-influenced input, but it does not neutralize or incorrectly neutralize special elements that can modify the intended OS command when it is sent to a downstream component.
CVE-2026-115886 (GHSA-2wqp-q3c6-ccj5) is a Server-Side Request Forgery (SSRF) and Unsafe Reflection vulnerability in MLflow versions 3.12.0rc0 prior to 3.17.0. Unauthenticated REST API endpoints accept serialized third-party scorer JSON payloads that are dynamically deserialized and reflectively instantiated without sanitizing constructor keyword arguments. When optional dependencies such as trulens and trulens-providers-litellm are installed, attackers can inject egress-steering arguments into the scorer payload, forcing the MLflow server to dispatch outbound HTTP requests to arbitrary internal destinations or cloud metadata services.
CVE-2026-113872 (GHSA-q7qp-rjg7-q26m) is a high-severity denial-of-service vulnerability affecting the multer multipart/form-data upload middleware for Node.js and Express in versions 2.0.0 through 2.4.0. When an endpoint is configured to accept multiple files using an asynchronous fileFilter callback, a crafted, truncated multipart HTTP request can trigger an unhandled error event on the underlying busboy stream parser after request teardown has already completed. Because all error listeners are stripped during teardown, Node.js treats the resulting error event as an uncaught exception and immediately terminates the server process.
CVE-2026-115897 (also tracked as GHSA-m5mm-p8h7-m29v and aliased as CVE-2026-115754) is a Cross-Site Scripting (XSS) vulnerability in the Yahoo serialize-javascript npm package prior to version 7.1.3. The flaw exists in the regular expression used by escapeFunctionBody() to neutralize HTML script closing tags within serialized JavaScript functions. Because function bodies are sanitized in isolation before structural formatting is applied, a function whose string representation terminates directly with the sequence '</script' bypasses the positive lookahead check. When combined with serialization formatting options that introduce whitespace delimiters and a subsequent property that supplies a closing bracket, an attacker can prematurely terminate an enclosing HTML <script> block and inject arbitrary HTML or JavaScript into the rendered document.
CVE-2026-109841 is a Subresource Integrity (SRI) verification bypass vulnerability in undici, the official HTTP/1.1 and HTTP/2 client library for Node.js that powers the global fetch() implementation. The flaw stems from improper handling of case sensitivity (CWE-178) when validating cryptographic hash algorithm tokens within SRI metadata. Because undici compared incoming algorithm identifiers against a strictly lowercase set without prior ASCII-lowercase normalization, valid uppercase or mixed-case tokens such as SHA256- or Sha384- were treated as unrecognized algorithms and discarded. Under SRI forward-compatibility rules, discarding all provided tokens results in an empty integrity list, causing undici to fail open and accept tampered HTTP response bodies without cryptographic verification.
A resource allocation vulnerability (CWE-770) exists in the Server-Sent Events (SSE) parser within the Node.js undici HTTP client library. In affected versions, the EventSourceStream implementation fails to restrict the count of internal Buffer chunk references and accumulated string nodes prior to event dispatch boundaries. A malicious or compromised SSE endpoint can exploit this behavior by streaming fragmented, unterminated lines or continuous sequences of short data fields without empty separator lines, bypassing configured byte-size limits and causing a fatal Out-of-Memory (OOM) crash in the client Node.js process.
CVE-2026-109869 is a high-severity denial-of-service vulnerability in undici, the default HTTP/1.1 client and Fetch implementation for Node.js. When a server responds with more than five Content-Encoding directives and prematurely closes the connection before transmitting the body, undici allocates an internal Readable stream before enforcing the content-encoding limit. Upon rejecting the fetch promise, the stream is orphaned without an error listener or abort signal, causing the premature socket closure to emit an unhandled stream error that terminates the Node.js process.