Skip to content

Hardening: Block path traversal in package install via composer.json name#237

Open
Copilot wants to merge 7 commits intomainfrom
copilot/hardening-path-traversal-issue
Open

Hardening: Block path traversal in package install via composer.json name#237
Copilot wants to merge 7 commits intomainfrom
copilot/hardening-path-traversal-issue

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 27, 2026

A malicious ZIP package with "name": ".." in its composer.json could cause wp package install to copy files outside the intended ~/.wp-cli/packages/local/ sandbox—landing payloads like vendor/autoload.php at ~/.wp-cli/packages/vendor/autoload.php, which WP-CLI loads on every invocation. The exploit fires before Composer validates the package name, so even a failed install leaves the payload on disk.

Changes

src/Package_Command.php

  • Package name validation (get_package_name_and_version_from_dir_package()): validate the name read from composer.json against Composer's canonical naming regex via ValidatingArrayLoader::hasPackageNamingError() before any file operations. Names like .. are rejected immediately.

  • Path containment check (install()): after computing the destination path, assert it stays within local_dir using a new is_child_path() helper. Violation throws an Exception caught by the existing cleanup handler.

// New validation in get_package_name_and_version_from_dir_package()
$naming_error = ValidatingArrayLoader::hasPackageNamingError( $package_name );
if ( null !== $naming_error ) {
    WP_CLI::error( sprintf( "Invalid package name '%s': %s", $package_name, $naming_error ) );
}

// New containment guard in install() before copy_overwrite_files()
if ( ! self::is_child_path( $actual_dir_package, $local_dir ) ) {
    throw new Exception( 'Invalid package: resolved destination path escapes the packages directory.' );
}

features/package-install.feature

Added a scenario that creates a ZIP with "name": ".." and a vendor/autoload.php payload, then asserts the install fails with the expected error before any files are written.


Note: The upstream Extractor::copy_overwrite_files() in wp-cli/wp-cli also lacks a base-path containment check (unlike its sibling Extractor::rmdir()). That hardening should be applied in the wp-cli/wp-cli repository as a complementary fix.

Copilot AI and others added 2 commits April 27, 2026 11:58
Copilot AI changed the title [WIP] Fix path traversal vulnerability in package install command Hardening: Block path traversal in package install via composer.json name Apr 27, 2026
Copilot AI requested a review from swissspidy April 27, 2026 12:01
@github-actions github-actions Bot added bug command:package-install Related to 'package install' command scope:distribution Related to distribution state:unconfirmed labels Apr 27, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 27, 2026

Codecov Report

❌ Patch coverage is 0% with 10 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/Package_Command.php 0.00% 10 Missing ⚠️

📢 Thoughts on this report? Let us know!

Comment thread features/package-install.feature Outdated
@swissspidy swissspidy marked this pull request as ready for review April 27, 2026 13:26
@swissspidy swissspidy requested a review from a team as a code owner April 27, 2026 13:26
Copilot AI review requested due to automatic review settings April 27, 2026 13:26
@swissspidy swissspidy added this to the 2.7.2 milestone Apr 27, 2026

This comment was marked as resolved.

Copilot AI requested a review from swissspidy April 27, 2026 13:42
@swissspidy

This comment was marked as resolved.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces security measures to prevent path-traversal attacks during ZIP package installation by validating package names and verifying that destination paths remain within the designated directory. However, the implementation has a few critical issues: the use of ValidatingArrayLoader::hasPackageNamingError() will cause fatal errors on environments running Composer 1.x, and the is_child_path helper is currently ineffective against traversal because it fails to resolve '..' components and does not account for case-insensitive file systems.

Comment thread src/Package_Command.php
Comment thread src/Package_Command.php
Comment thread features/package-install.feature Outdated
Copilot AI requested a review from swissspidy April 27, 2026 14:24
Comment thread src/Package_Command.php
Comment on lines +1211 to +1215
private static function is_child_path( $path, $parent_dir ) {
$normalized_path = rtrim( Path::normalize( $path ), '/' ) . '/';
$normalized_parent = rtrim( Path::normalize( $parent_dir ), '/' ) . '/';
return 0 === strpos( $normalized_path, $normalized_parent );
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot

The is_child_path helper has two significant limitations for a security guard:

  1. Lack of .. resolution: Path::normalize() in WP-CLI does not resolve .. components. A path like /base/local/../ will still be considered a child of /base/local/ because the string starts with the parent path. This makes the check ineffective against the very traversal it aims to prevent if the input contains ...
  2. Case Sensitivity: strpos() is case-sensitive. On Windows or macOS, this check might fail if the paths have inconsistent casing (e.g., C:\ vs c:\), leading to false positives.

Consider resolving .. components manually (since the path might not exist on disk) and using a case-insensitive comparison where appropriate.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ab278f6. Replaced Path::normalize() + strpos() with:

  1. A new resolve_dot_segments() helper that manually walks the path segments and collapses .. without touching the filesystem — so /base/local/../vendor correctly resolves to /base/vendor, which then fails the containment check.
  2. stripos() instead of strpos() on Windows (DIRECTORY_SEPARATOR === '\\') for case-insensitive path comparison.

…rison on Windows

Agent-Logs-Url: https://github.com/wp-cli/package-command/sessions/c458f3cb-4cae-4b57-b796-a60a092eea37

Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com>
Copilot AI requested a review from swissspidy April 27, 2026 20:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug command:package-install Related to 'package install' command scope:distribution Related to distribution state:unconfirmed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Hardening: Path traversal in package install via composer.json name

3 participants