A security report highlighted a weakness in the wp package install <URL> command where a malicious ZIP package can achieve persistent Remote Code Execution (RCE) by exploiting a path traversal vulnerability.
While WP-CLI commands are highly privileged and installing untrusted packages is inherently risky, this specific issue violates the expectation that a failed installation should not leave persistent side effects. In this case, even when Composer rejects the package name and the install fails, a malicious file is already placed outside the intended sandbox.
Technical Details
- Path Traversal: The flaw lies in
src/Package_Command.php. When installing a package from a URL or local ZIP, WP-CLI reads the composer.json from the extracted files. If the name field is set to .., the path resolved for moving the files can escape the intended directory.
- Autoloader Overwrite: An attacker can place a malicious
vendor/autoload.php file in the ZIP. Due to the path traversal, this file lands at ~/.wp-cli/packages/vendor/autoload.php, which is the aggregate autoloader that WP-CLI loads on every subsequent invocation (unless --skip-packages is used).
- Missing Containment Check: The framework function
Extractor::copy_overwrite_files() in php/WP_CLI/Extractor.php lacks a base-path containment check. It copies files without verifying that the resolved destination stays under the target directory. A sibling function, Extractor::rmdir(), already implements such a check.
- Race Condition/Timing: The copy operation happens before Composer validates the
composer.json file. Therefore, even though Composer eventually fails the installation because .. is not a valid package name, the payload is already on disk.
Proposed Hardening
- Validate Package Name: In
src/Package_Command.php, validate the package name read from composer.json against Composer's canonical regex before performing any file operations. Reject invalid names immediately.
- Add Containment Checks: Harden
Extractor::copy_overwrite_files() by implementing a base-path containment check using realpath(), mirroring the logic already present in Extractor::rmdir().
A security report highlighted a weakness in the
wp package install <URL>command where a malicious ZIP package can achieve persistent Remote Code Execution (RCE) by exploiting a path traversal vulnerability.While WP-CLI commands are highly privileged and installing untrusted packages is inherently risky, this specific issue violates the expectation that a failed installation should not leave persistent side effects. In this case, even when Composer rejects the package name and the install fails, a malicious file is already placed outside the intended sandbox.
Technical Details
src/Package_Command.php. When installing a package from a URL or local ZIP, WP-CLI reads thecomposer.jsonfrom the extracted files. If thenamefield is set to.., the path resolved for moving the files can escape the intended directory.vendor/autoload.phpfile in the ZIP. Due to the path traversal, this file lands at~/.wp-cli/packages/vendor/autoload.php, which is the aggregate autoloader that WP-CLI loads on every subsequent invocation (unless--skip-packagesis used).Extractor::copy_overwrite_files()inphp/WP_CLI/Extractor.phplacks a base-path containment check. It copies files without verifying that the resolved destination stays under the target directory. A sibling function,Extractor::rmdir(), already implements such a check.composer.jsonfile. Therefore, even though Composer eventually fails the installation because..is not a valid package name, the payload is already on disk.Proposed Hardening
src/Package_Command.php, validate the package name read fromcomposer.jsonagainst Composer's canonical regex before performing any file operations. Reject invalid names immediately.Extractor::copy_overwrite_files()by implementing a base-path containment check usingrealpath(), mirroring the logic already present inExtractor::rmdir().