From 8f1faaa665140ae4e6fa022c0bbbf618812382af Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sat, 23 May 2026 16:06:21 +0200 Subject: [PATCH 1/8] Detect file extension from downloaded archives --- features/core-download.feature | 37 ++++++++++++++++++++++++++++++++++ src/Core_Command.php | 37 +++++++++++++++++++++++++++++++--- 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/features/core-download.feature b/features/core-download.feature index 1c5b6ad0..13f5f009 100644 --- a/features/core-download.feature +++ b/features/core-download.feature @@ -556,3 +556,40 @@ Feature: Download WordPress Success: """ + Scenario: Extracts provided tar.gz files + Given an empty directory + + When I run `wp core download https://downloads.wordpress.org/release/wordpress-7.0.tar.gz --force` + Then the {RUN_DIR} directory should contain: + """ + index.php + license.txt + """ + + Scenario: Extracts provided zip files + Given an empty directory + + When I run `wp core download https://downloads.wordpress.org/release/wordpress-7.0.zip --force` + Then the {RUN_DIR} directory should contain: + """ + index.php + license.txt + """ + + Scenario: Error when downloading an unsupported archive format + Given an empty directory + And that HTTP requests to http://example.com/unsupported.txt will respond with: + """ + HTTP/1.1 200 OK + Content-Type: text/plain + + This is not a zip or tarball file. + """ + + When I try `wp core download http://example.com/unsupported.txt --force` + Then STDERR should contain: + """ + Error: Unsupported archive format. The downloaded file is not a valid zip or tar.gz archive. + """ + And the return code should be 1 + diff --git a/src/Core_Command.php b/src/Core_Command.php index 25003af9..137e7f4d 100644 --- a/src/Core_Command.php +++ b/src/Core_Command.php @@ -297,11 +297,11 @@ public function download( $args, $assoc_args ) { } if ( ! $cache_file || $bad_cache ) { - $temp = Utils\get_temp_dir() . uniqid( 'wp_' ) . ".{$extension}"; + $temp = Utils\get_temp_dir() . uniqid( 'wp_' ) . '.tmp'; register_shutdown_function( - function () use ( $temp ) { + function () use ( &$temp ) { if ( file_exists( $temp ) ) { - unlink( $temp ); + @unlink( $temp ); } } ); @@ -322,6 +322,37 @@ function () use ( $temp ) { WP_CLI::error( "Couldn't access download URL (HTTP code {$response->status_code})." ); } + $extension = ''; + if ( file_exists( $temp ) ) { + $mime = function_exists( 'mime_content_type' ) ? mime_content_type( $temp ) : ''; + if ( 'application/zip' === $mime || 'application/x-zip-compressed' === $mime ) { + $extension = 'zip'; + } elseif ( 'application/x-gzip' === $mime || 'application/gzip' === $mime ) { + $extension = 'tar.gz'; + } else { + // Fallback to magic bytes. + $handle = @fopen( $temp, 'rb' ); + if ( $handle ) { + $bytes = fread( $handle, 2 ); + fclose( $handle ); + if ( 'PK' === $bytes ) { + $extension = 'zip'; + } elseif ( "\x1f\x8b" === $bytes ) { + $extension = 'tar.gz'; + } + } + } + } + + if ( ! in_array( $extension, [ 'zip', 'tar.gz' ], true ) ) { + WP_CLI::error( 'Unsupported archive format. The downloaded file is not a valid zip or tar.gz archive.' ); + } + + $actual_temp = substr( $temp, 0, -4 ) . ".{$extension}"; + if ( rename( $temp, $actual_temp ) ) { + $temp = $actual_temp; + } + if ( 'nightly' !== $version ) { unset( $options['filename'] ); /** @var \WpOrg\Requests\Response $md5_response */ From 623b43b849dc3f7491fd3cc9a823cdb872bbf5ef Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sat, 23 May 2026 16:15:49 +0200 Subject: [PATCH 2/8] Fix indentation --- features/core-download.feature | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/features/core-download.feature b/features/core-download.feature index 13f5f009..154eb897 100644 --- a/features/core-download.feature +++ b/features/core-download.feature @@ -561,20 +561,20 @@ Feature: Download WordPress When I run `wp core download https://downloads.wordpress.org/release/wordpress-7.0.tar.gz --force` Then the {RUN_DIR} directory should contain: - """ - index.php - license.txt - """ + """ + index.php + license.txt + """ Scenario: Extracts provided zip files Given an empty directory When I run `wp core download https://downloads.wordpress.org/release/wordpress-7.0.zip --force` Then the {RUN_DIR} directory should contain: - """ - index.php - license.txt - """ + """ + index.php + license.txt + """ Scenario: Error when downloading an unsupported archive format Given an empty directory From 797b0a06de3c264d93dac8fabf3754a051a834ed Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sat, 23 May 2026 16:19:47 +0200 Subject: [PATCH 3/8] Address code review feedback --- src/Core_Command.php | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/Core_Command.php b/src/Core_Command.php index 137e7f4d..bf62001e 100644 --- a/src/Core_Command.php +++ b/src/Core_Command.php @@ -342,6 +342,19 @@ function () use ( &$temp ) { } } } + + // Deep validation for tar.gz archives: verify ustar magic string in decompressed stream. + if ( 'tar.gz' === $extension && function_exists( 'gzopen' ) ) { + // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Silence potential gzopen warnings on corrupt streams. + $gz = @gzopen( $temp, 'rb' ); + if ( $gz ) { + $header = gzread( $gz, 262 ); + gzclose( $gz ); + if ( ! is_string( $header ) || 'ustar' !== substr( $header, 257, 5 ) ) { + $extension = ''; + } + } + } } if ( ! in_array( $extension, [ 'zip', 'tar.gz' ], true ) ) { @@ -349,7 +362,15 @@ function () use ( &$temp ) { } $actual_temp = substr( $temp, 0, -4 ) . ".{$extension}"; - if ( rename( $temp, $actual_temp ) ) { + if ( ! rename( $temp, $actual_temp ) ) { + // Fallback to copy + unlink. + if ( copy( $temp, $actual_temp ) ) { + unlink( $temp ); + $temp = $actual_temp; + } else { + WP_CLI::error( 'Failed to rename the downloaded temporary file.' ); + } + } else { $temp = $actual_temp; } From 58386023a5fccd05a7668448199e4eefd4b35dcf Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sat, 23 May 2026 22:31:20 +0200 Subject: [PATCH 4/8] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/Core_Command.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Core_Command.php b/src/Core_Command.php index bf62001e..d1bf354d 100644 --- a/src/Core_Command.php +++ b/src/Core_Command.php @@ -322,6 +322,10 @@ function () use ( &$temp ) { WP_CLI::error( "Couldn't access download URL (HTTP code {$response->status_code})." ); } + if ( ! file_exists( $temp ) || ! is_readable( $temp ) ) { + WP_CLI::error( "Downloaded file could not be written to or read from disk: {$temp}" ); + } + $extension = ''; if ( file_exists( $temp ) ) { $mime = function_exists( 'mime_content_type' ) ? mime_content_type( $temp ) : ''; From 9d91ab8bce22714d4815c0b2d41538b3cc0406eb Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sat, 23 May 2026 22:33:24 +0200 Subject: [PATCH 5/8] Lint fix --- src/Core_Command.php | 52 +++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/src/Core_Command.php b/src/Core_Command.php index d1bf354d..9471e482 100644 --- a/src/Core_Command.php +++ b/src/Core_Command.php @@ -327,36 +327,34 @@ function () use ( &$temp ) { } $extension = ''; - if ( file_exists( $temp ) ) { - $mime = function_exists( 'mime_content_type' ) ? mime_content_type( $temp ) : ''; - if ( 'application/zip' === $mime || 'application/x-zip-compressed' === $mime ) { - $extension = 'zip'; - } elseif ( 'application/x-gzip' === $mime || 'application/gzip' === $mime ) { - $extension = 'tar.gz'; - } else { - // Fallback to magic bytes. - $handle = @fopen( $temp, 'rb' ); - if ( $handle ) { - $bytes = fread( $handle, 2 ); - fclose( $handle ); - if ( 'PK' === $bytes ) { - $extension = 'zip'; - } elseif ( "\x1f\x8b" === $bytes ) { - $extension = 'tar.gz'; - } + $mime = function_exists( 'mime_content_type' ) ? mime_content_type( $temp ) : ''; + if ( 'application/zip' === $mime || 'application/x-zip-compressed' === $mime ) { + $extension = 'zip'; + } elseif ( 'application/x-gzip' === $mime || 'application/gzip' === $mime ) { + $extension = 'tar.gz'; + } else { + // Fallback to magic bytes. + $handle = @fopen( $temp, 'rb' ); + if ( $handle ) { + $bytes = fread( $handle, 2 ); + fclose( $handle ); + if ( 'PK' === $bytes ) { + $extension = 'zip'; + } elseif ( "\x1f\x8b" === $bytes ) { + $extension = 'tar.gz'; } } + } - // Deep validation for tar.gz archives: verify ustar magic string in decompressed stream. - if ( 'tar.gz' === $extension && function_exists( 'gzopen' ) ) { - // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Silence potential gzopen warnings on corrupt streams. - $gz = @gzopen( $temp, 'rb' ); - if ( $gz ) { - $header = gzread( $gz, 262 ); - gzclose( $gz ); - if ( ! is_string( $header ) || 'ustar' !== substr( $header, 257, 5 ) ) { - $extension = ''; - } + // Deep validation for tar.gz archives: verify ustar magic string in decompressed stream. + if ( 'tar.gz' === $extension && function_exists( 'gzopen' ) ) { + // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Silence potential gzopen warnings on corrupt streams. + $gz = @gzopen( $temp, 'rb' ); + if ( $gz ) { + $header = gzread( $gz, 262 ); + gzclose( $gz ); + if ( ! is_string( $header ) || 'ustar' !== substr( $header, 257, 5 ) ) { + $extension = ''; } } } From 31f76e91a98839f1f12192733063cca149f8b36f Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sat, 23 May 2026 22:43:58 +0200 Subject: [PATCH 6/8] Address code review feedback --- src/Core_Command.php | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/Core_Command.php b/src/Core_Command.php index 9471e482..454d8ce7 100644 --- a/src/Core_Command.php +++ b/src/Core_Command.php @@ -346,16 +346,18 @@ function () use ( &$temp ) { } } - // Deep validation for tar.gz archives: verify ustar magic string in decompressed stream. + // Deep validation for tar.gz archives: verify it's a valid, readable gzip stream. if ( 'tar.gz' === $extension && function_exists( 'gzopen' ) ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Silence potential gzopen warnings on corrupt streams. $gz = @gzopen( $temp, 'rb' ); if ( $gz ) { $header = gzread( $gz, 262 ); gzclose( $gz ); - if ( ! is_string( $header ) || 'ustar' !== substr( $header, 257, 5 ) ) { + if ( ! is_string( $header ) || strlen( $header ) < 1 ) { $extension = ''; } + } else { + $extension = ''; } } @@ -367,10 +369,17 @@ function () use ( &$temp ) { if ( ! rename( $temp, $actual_temp ) ) { // Fallback to copy + unlink. if ( copy( $temp, $actual_temp ) ) { - unlink( $temp ); - $temp = $actual_temp; - } else { - WP_CLI::error( 'Failed to rename the downloaded temporary file.' ); + $old_temp = $temp; + $temp = $actual_temp; + if ( ! unlink( $old_temp ) ) { + register_shutdown_function( + function () use ( $old_temp ) { + if ( file_exists( $old_temp ) ) { + unlink( $old_temp ); + } + } + ); + } } } else { $temp = $actual_temp; From c2d45129cc66bedb3c54f665c4529e0f08aebfac Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 24 May 2026 12:13:51 +0200 Subject: [PATCH 7/8] no suppression --- src/Core_Command.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core_Command.php b/src/Core_Command.php index 454d8ce7..1348dac8 100644 --- a/src/Core_Command.php +++ b/src/Core_Command.php @@ -301,7 +301,7 @@ public function download( $args, $assoc_args ) { register_shutdown_function( function () use ( &$temp ) { if ( file_exists( $temp ) ) { - @unlink( $temp ); + unlink( $temp ); } } ); From e1b335af0a7d292c34a3b63e91f7be748a1b04a5 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 24 May 2026 12:22:46 +0200 Subject: [PATCH 8/8] add error --- src/Core_Command.php | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/Core_Command.php b/src/Core_Command.php index 1348dac8..b65f7c01 100644 --- a/src/Core_Command.php +++ b/src/Core_Command.php @@ -368,18 +368,19 @@ function () use ( &$temp ) { $actual_temp = substr( $temp, 0, -4 ) . ".{$extension}"; if ( ! rename( $temp, $actual_temp ) ) { // Fallback to copy + unlink. - if ( copy( $temp, $actual_temp ) ) { - $old_temp = $temp; - $temp = $actual_temp; - if ( ! unlink( $old_temp ) ) { - register_shutdown_function( - function () use ( $old_temp ) { - if ( file_exists( $old_temp ) ) { - unlink( $old_temp ); - } + if ( ! copy( $temp, $actual_temp ) ) { + WP_CLI::error( 'Failed to copy the downloaded file.' ); + } + $old_temp = $temp; + $temp = $actual_temp; + if ( ! unlink( $old_temp ) ) { + register_shutdown_function( + function () use ( $old_temp ) { + if ( file_exists( $old_temp ) ) { + unlink( $old_temp ); } - ); - } + } + ); } } else { $temp = $actual_temp;