Skip to content

Commit 78e54f7

Browse files
Copilotswissspidy
andcommitted
Fix security validations: properly check path traversal and validate plugin headers
Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com>
1 parent 0b4d7aa commit 78e54f7

1 file changed

Lines changed: 25 additions & 13 deletions

File tree

src/WP_CLI/CommandWithUpgrade.php

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -363,15 +363,24 @@ protected function install_from_php_file( $url, $assoc_args ) {
363363
return new WP_Error( 'invalid_filename', 'The sanitized filename does not have a .php extension.' );
364364
}
365365

366+
// Ensure plugin directory exists.
367+
if ( ! is_dir( WP_PLUGIN_DIR ) ) {
368+
wp_mkdir_p( WP_PLUGIN_DIR );
369+
}
370+
366371
// Validate the destination stays within the plugin directory.
367372
$dest_path = trailingslashit( WP_PLUGIN_DIR ) . $dest_filename;
368373
$real_dest = realpath( WP_PLUGIN_DIR );
369-
if ( false !== $real_dest ) {
370-
// Ensure destination is within plugin directory (prevent directory traversal).
371-
$dest_dir = dirname( $dest_path );
372-
if ( 0 !== strpos( $dest_dir, $real_dest ) ) {
373-
return new WP_Error( 'invalid_path', 'The destination path is outside the plugin directory.' );
374-
}
374+
$real_path = realpath( dirname( $dest_path ) );
375+
376+
// Ensure plugin directory and destination parent directory are valid.
377+
if ( false === $real_dest || false === $real_path ) {
378+
return new WP_Error( 'invalid_path', 'Cannot validate plugin directory path.' );
379+
}
380+
381+
// Ensure destination is within plugin directory (prevent directory traversal).
382+
if ( 0 !== strpos( $real_path, $real_dest ) ) {
383+
return new WP_Error( 'invalid_path', 'The destination path is outside the plugin directory.' );
375384
}
376385

377386
// Display info message before downloading.
@@ -384,11 +393,16 @@ protected function install_from_php_file( $url, $assoc_args ) {
384393
return new WP_Error( 'download_failed', sprintf( 'Could not download PHP file from %s: %s', esc_url( $url ), $temp_file->get_error_message() ) );
385394
}
386395

387-
// Read the plugin headers from the downloaded file.
396+
// Verify the downloaded file is a valid PHP file with plugin headers.
388397
$plugin_data = get_plugin_data( $temp_file, false, false );
389398

390-
// If no plugin name is found, use the filename.
391-
$plugin_name = ! empty( $plugin_data['Name'] ) ? $plugin_data['Name'] : '';
399+
// Verify this is actually a plugin file with at least a plugin name.
400+
if ( empty( $plugin_data['Name'] ) ) {
401+
unlink( $temp_file );
402+
return new WP_Error( 'invalid_plugin', 'The downloaded file does not appear to be a valid WordPress plugin.' );
403+
}
404+
405+
$plugin_name = $plugin_data['Name'];
392406

393407
// Check if plugin is already installed.
394408
if ( file_exists( $dest_path ) && ! Utils\get_flag_value( $assoc_args, 'force' ) ) {
@@ -398,10 +412,8 @@ protected function install_from_php_file( $url, $assoc_args ) {
398412
}
399413

400414
// Display plugin info.
401-
if ( ! empty( $plugin_name ) ) {
402-
$version = ! empty( $plugin_data['Version'] ) ? $plugin_data['Version'] : '';
403-
WP_CLI::log( sprintf( 'Installing %s%s', $plugin_name, $version ? " ($version)" : '' ) );
404-
}
415+
$version = ! empty( $plugin_data['Version'] ) ? $plugin_data['Version'] : '';
416+
WP_CLI::log( sprintf( 'Installing %s%s', $plugin_name, $version ? " ($version)" : '' ) );
405417

406418
// Move the file to the plugins directory.
407419
$result = copy( $temp_file, $dest_path );

0 commit comments

Comments
 (0)