Make WordPress Core

Opened 3 years ago

Closed 3 years ago

Last modified 3 years ago

#55307 closed defect (bug) (duplicate)

After upgrading Debian, WordPress sites no longer update through FTP

Reported by: zippy1970's profile zippy1970 Owned by:
Milestone: Priority: normal
Severity: major Version: 6.0
Component: Upgrade/Install Keywords:
Focuses: Cc:

Description

I upgraded my production server from Debian 10 (Buster) to Debian 11 (Bullseye) a few days ago. At first, none of the hosted WordPress sites worked. All gave me an error about a missing mysql module. As it turned out, I needed to install php7.4-mysqli.

But then when I wanted to update the WordPress sites from v5.9.0 to v5.9.1, all failed:

Downloading update from https://downloads.wordpress.org/release/wordpress-5.9.1-no-content.zip…

The authenticity of wordpress-5.9.1-no-content.zip could not be verified as no signature was found.

Unpacking the update…

Verifying the unpacked files…

The update could not be unpacked.

Installation failed.

I took me quite a while to figure out what actually was going on. Now I know, I have no idea if this is a bug in WordPress, or caused by something else.

The error message WP is giving, is actually incorrect. As it turns out, it is able to unpack the update just fine in the wp-content/upgrade folder. But it’s when it checks if the files have actually unpacked, where it goes wrong. The problem lies in this piece of code in update-core.php:

<?php
  foreach ( $roots as $root ) {
    if ( $wp_filesystem->exists( $from . $root . 'readme.html' )
       && $wp_filesystem->exists( $from . $root . 'wp-includes/version.php' )
     ) {
       $distro = $root;
       break;
     }
   }

   if ( ! $distro ) {
     $wp_filesystem->delete( $from, true );
     return new WP_Error( 'insane_distro', __( 'The update could not be unpacked.' ) );
   }

What it does here, is simply check if two files exists in the folder it has just unpacked the zip file to. This fails. And the reason is as follows:

I use FTP method for installing updates. So when I tell it to update, it first figures out the folder it should download the zipfile to. This folder is stored in $working_dir in wp-admin/includes/class-wp-upgrader.php -> unpack_package(). The full path on the server is /domains/domainname.com/htdocs/wp-content/upgrade/ but since FTP users are chrooted, WP finds and stores /htdocs/wp-content/upgrade/ instead. The update file is downloaded to this folder and then unpacked.

Next it does the above check in wp-admin/includes/update-core.php -> update_core(). And that failes because it tries to find a file in /htdocs/wp-content/upgrade/ while the true location is /domains/domainname.com/htdocs/wp-content/upgrade/.

Updating through FTP used to work fine in Debian 10 but in Debian 11, something broke...

Change History (19)

#1 follow-up: @costdev
3 years ago

  • Keywords needs-testing added

Hi @zippy1970, welcome to Trac!

I'll add needs-testing so that we can try to get the issue reproduced and see where the problem should be resolved.

In the meantime, would you be able to set up a website using WordPress 5.7.5 on Debian 11 and try to upgrade to 5.8? This would let us confirm that the issue only lies within Debian 11, and not a mixture of Debian 11 and WordPress 5.9, and allow us to assign the Version property of the ticket accurately.

#2 follow-up: @afragen
3 years ago

Is there some reason you must use the FTP option to upgrade? Does upgrading work if you set the following in your
wp-config.php?

define('FS_METHOD','direct');

#3 in reply to: ↑ 2 @zippy1970
3 years ago

Replying to afragen:

Is there some reason you must use the FTP option to upgrade?

If I use the direct method, I first get this:

Downloading update from https://downloads.wordpress.org/release/wordpress-5.9.1-no-content.zip…

The authenticity of wordpress-5.9.1-no-content.zip could not be verified as no signature was found.

Unpacking the update…

Could not create directory.

Installation failed.

So next I do chmod 0777 wp-content/upgrade and then I get this:

Downloading update from https://downloads.wordpress.org/release/wordpress-5.9.1-no-content.zip…

The authenticity of wordpress-5.9.1-no-content.zip could not be verified as no signature was found.

Unpacking the update…

Verifying the unpacked files…

Preparing to install the latest version…

The update cannot be installed because we will be unable to copy some files. This is usually due to inconsistent file permissions.: wp-includes/block-supports/elements.php, wp-includes/block-supports/duotone.php, wp-includes/block-supports/layout.php, wp-includes/default-filters.php, wp-includes/block-template.php, wp-includes/css/dist/edit-site/style-rtl.min.css, wp-includes/css/dist/edit-site/style.min.css, wp-includes/css/dist/edit-site/style-rtl.css, wp-includes/css/dist/edit-site/style.css, wp-includes/css/dist/block-library/editor.min.css, wp-includes/css/dist/block-library/style-rtl.min.css, wp-includes/css/dist/block-library/common-rtl.min.css, wp-includes/css/dist/block-library/editor.css, wp-includes/css/dist/block-library/common.css, wp-includes/css/dist/block-library/style.min.css, wp-includes/css/dist/block-library/editor-rtl.css, wp-includes/css/dist/block-library/editor-rtl.min.css, wp-includes/css/dist/block-library/style-rtl.css, wp-includes/css/dist/block-library/common.min.css, wp-includes/css/dist/block-library/common-rtl.css, wp-includes/css/dist/block-library/style.css, wp-includes/css/dist/components/style-rtl.min.css, wp-includes/css/dist/components/style.min.css, wp-includes/css/dist/components/style-rtl.css, wp-includes/css/dist/components/style.css, wp-includes/css/dist/block-editor/style-rtl.min.css, wp-includes/css/dist/block-editor/style.min.css, wp-includes/css/dist/block-editor/style-rtl.css, wp-includes/css/dist/block-editor/style.css, wp-includes/class-wp-theme-json.php, wp-includes/script-loader.php, wp-includes/assets/script-loader-packages.php, wp-includes/blocks/gallery/style-rtl.min.css, wp-includes/blocks/gallery/style.min.css, wp-includes/blocks/gallery/style-rtl.css, wp-includes/blocks/gallery/style.css, wp-includes/blocks/site-title.php, wp-includes/blocks/page-list/editor.min.css, wp-includes/blocks/page-list/editor.css, wp-includes/blocks/page-list/editor-rtl.css, wp-includes/blocks/page-list/editor-rtl.min.css, wp-includes/blocks/page-list.php, wp-includes/blocks/search.php, wp-includes/blocks/post-template/style-rtl.min.css, wp-includes/blocks/post-template/style.min.css, wp-includes/blocks/post-template/style-rtl.css, wp-includes/blocks/post-template/style.css, wp-includes/blocks/post-featured-image/editor.min.css, wp-includes/blocks/post-featured-image/editor.css, wp-includes/blocks/post-featured-image/editor-rtl.css, wp-includes/blocks/post-featured-image/editor-rtl.min.css, wp-includes/blocks/index.php, wp-includes/blocks/image/style-rtl.min.css, wp-includes/blocks/image/style.min.css, wp-includes/blocks/image/style-rtl.css, wp-includes/blocks/image/style.css, wp-includes/blocks/cover/style-rtl.min.css, wp-includes/blocks/cover/style.min.css, wp-includes/blocks/cover/style-rtl.css, wp-includes/blocks/cover/style.css, wp-includes/blocks/site-logo.php, wp-includes/blocks/spacer/editor.min.css, wp-includes/blocks/spacer/block.json, wp-includes/blocks/spacer/editor.css, wp-includes/blocks/spacer/editor-rtl.css, wp-includes/blocks/spacer/editor-rtl.min.css, wp-includes/deprecated.php, wp-includes/version.php, wp-includes/class-wp-theme-json-resolver.php, wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php, wp-includes/block-editor.php, wp-includes/post.php, wp-includes/global-styles-and-settings.php, wp-includes/js/dist/core-data.js, wp-includes/js/dist/components.js, wp-includes/js/dist/edit-site.min.js, wp-includes/js/dist/block-library.min.js, wp-includes/js/dist/edit-post.min.js, wp-includes/js/dist/block-editor.min.js, wp-includes/js/dist/core-data.min.js, wp-includes/js/dist/components.min.js, wp-includes/js/dist/editor.min.js, wp-includes/js/dist/edit-site.js, wp-includes/js/dist/block-editor.js, wp-includes/js/dist/block-library.js, wp-includes/js/dist/editor.js, wp-includes/js/dist/edit-post.js, wp-includes/js/wp-ajax-response.min.js, wp-includes/js/jquery/ui/effect-fade.js, wp-includes/js/jquery/ui/controlgroup.min.js, wp-includes/js/jquery/ui/effect-highlight.min.js, wp-includes/js/jquery/ui/effect-clip.js, wp-includes/js/jquery/ui/effect-highlight.js, wp-includes/js/jquery/ui/button.js, wp-includes/js/jquery/ui/tabs.min.js, wp-includes/js/jquery/ui/selectable.min.js, wp-includes/js/jquery/ui/draggable.js, wp-includes/js/jquery/ui/droppable.min.js, wp-includes/js/jquery/ui/effect.js, wp-includes/js/jquery/ui/spinner.js, wp-includes/js/jquery/ui/effect-explode.min.js, wp-includes/js/jquery/ui/accordion.js, wp-includes/js/jquery/ui/core.min.js, wp-includes/js/jquery/ui/effect-pulsate.min.js, wp-includes/js/jquery/ui/mouse.min.js, wp-includes/js/jquery/ui/effect-puff.min.js, wp-includes/js/jquery/ui/effect-shake.js, wp-includes/js/jquery/ui/menu.js, wp-includes/js/jquery/ui/draggable.min.js, wp-includes/js/jquery/ui/selectmenu.js, wp-includes/js/jquery/ui/sortable.js, wp-includes/js/jquery/ui/core.js, wp-includes/js/jquery/ui/effect-slide.min.js, wp-includes/js/jquery/ui/effect-drop.js, wp-includes/js/jquery/ui/effect-size.js, wp-includes/js/jquery/ui/autocomplete.js, wp-includes/js/jquery/ui/menu.min.js, wp-includes/js/jquery/ui/tabs.js, wp-includes/js/jquery/ui/effect-bounce.min.js, wp-includes/js/jquery/ui/effect-drop.min.js, wp-includes/js/jquery/ui/selectable.js, wp-includes/js/jquery/ui/dialog.js, wp-includes/js/jquery/ui/effect-fold.min.js, wp-includes/js/jquery/ui/checkboxradio.min.js, wp-includes/js/jquery/ui/effect-puff.js, wp-includes/js/jquery/ui/autocomplete.min.js, wp-includes/js/jquery/ui/slider.js, wp-includes/js/jquery/ui/tooltip.min.js, wp-includes/js/jquery/ui/sortable.min.js, wp-includes/js/jquery/ui/droppable.js, wp-includes/js/jquery/ui/effect-blind.min.js, wp-includes/js/jquery/ui/effect-pulsate.js, wp-includes/js/jquery/ui/resizable.min.js, wp-includes/js/jquery/ui/datepicker.js, wp-includes/js/jquery/ui/effect-scale.js, wp-includes/js/jquery/ui/checkboxradio.js, wp-includes/js/jquery/ui/spinner.min.js, wp-includes/js/jquery/ui/button.min.js, wp-includes/js/jquery/ui/progressbar.min.js, wp-includes/js/jquery/ui/tooltip.js, wp-includes/js/jquery/ui/effect.min.js, wp-includes/js/jquery/ui/effect-transfer.js, wp-includes/js/jquery/ui/accordion.min.js, wp-includes/js/jquery/ui/effect-fade.min.js, wp-includes/js/jquery/ui/effect-fold.js, wp-includes/js/jquery/ui/effect-transfer.min.js, wp-includes/js/jquery/ui/mouse.js, wp-includes/js/jquery/ui/controlgroup.js, wp-includes/js/jquery/ui/progressbar.js, wp-includes/js/jquery/ui/effect-clip.min.js, wp-includes/js/jquery/ui/effect-scale.min.js, wp-includes/js/jquery/ui/selectmenu.min.js, wp-includes/js/jquery/ui/effect-blind.js, wp-includes/js/jquery/ui/effect-explode.js, wp-includes/js/jquery/ui/resizable.js, wp-includes/js/jquery/ui/dialog.min.js, wp-includes/js/jquery/ui/effect-slide.js, wp-includes/js/jquery/ui/slider.min.js, wp-includes/js/jquery/ui/effect-bounce.js, wp-includes/js/jquery/ui/effect-size.min.js, wp-includes/js/jquery/ui/datepicker.min.js, wp-includes/js/jquery/ui/effect-shake.min.js, wp-includes/js/wp-ajax-response.js, wp-includes/option.php, wp-admin/css/dashboard-rtl.css, wp-admin/css/dashboard-rtl.min.css, wp-admin/css/dashboard.min.css, wp-admin/css/dashboard.css, wp-admin/about.php, wp-admin/update-core.php, wp-admin/includes/upgrade.php, wp-admin/includes/update-core.php, wp-admin/includes/class-core-upgrader.php, wp-admin/includes/file.php, wp-admin/edit-form-blocks.php

Installation failed.

I'm sure I can get this to work by setting the proper file permissions, but I remember now why I chose FTP: security. Can't remember exactly what the security concern was, but this is a webserver with multiple virtual hosts all running WordPress.

#4 in reply to: ↑ 1 @zippy1970
3 years ago

Replying to costdev:

In the meantime, would you be able to set up a website using WordPress 5.7.5 on Debian 11 and try to upgrade to 5.8? This would let us confirm that the issue only lies within Debian 11, and not a mixture of Debian 11 and WordPress 5.9, and allow us to assign the Version property of the ticket accurately.

I'll try setting up a website using 5.7.5 and try to upgrade it to 5.8. Will return with the results.

#5 @zippy1970
3 years ago

Ok, I installed WordPress 5.7.5 and tried to update it to 5.9.1. Got the exact same error:

Downloading update from https://downloads.wordpress.org/release/wordpress-5.9.1-new-bundled.zip…

The authenticity of wordpress-5.9.1-new-bundled.zip could not be verified as no signature was found.

Unpacking the update…

Verifying the unpacked files…

The update could not be unpacked

Installation failed.

#6 @zippy1970
3 years ago

Oh wait, did you want me to try to update to 5.8 or 5.9?

EDIT:
I tried upgrading to 5.8 (using the WP Upgrade plugin) and got the exact same error.

Last edited 3 years ago by zippy1970 (previous) (diff)

#7 follow-up: @afragen
3 years ago

Did you need to setup the chroot after your Debian upgrade? It seems that might be where your issue is, especially as downloaded file are in the expected locations.

#8 in reply to: ↑ 7 @zippy1970
3 years ago

No, FTP users have always been chrooted. Nothing changed there.

#9 @costdev
3 years ago

  • Version changed from 5.9 to trunk

Ok so we can be sure that it's not a 5.9 issue as the error occurs when upgrading from 5.7.5 to 5.8.

Changing version to trunk for now while we determine if something needs to change in Core, your permissions, or if it's a Debian 11 issue.

From Changing File Permissions:

If WordPress is running as the FTP account, that account needs to have write access, i.e., be the owner of the files, or belong to a group that has write access. In the latter case, that would mean permissions are set more permissively than default (for example, 775 rather than 755 for folders, and 664 instead of 644).

Let's see if the issue still occurs if you use 775 for folders and 664 for files:

cd /path/to/wordpress
find . -type d -exec chmod 775 {} \;
find . -type f -exec chmod 664 {} \;

#10 @zippy1970
3 years ago

Actually, I've done some testing and I now know exactly what is causing the problem.

ftp_nlist() always returns false when the second parameter is a file instead of a directory on my system.

<?php
$ftp_server = "ftp.myserver.com";
$ftp_user_name = "myusername";
$ftp_user_pass = "mypassword";

$ftp = ftp_connect($ftp_server);
$login_result = ftp_login($ftp, $ftp_user_name, $ftp_user_pass);

// check connection
if ((!$ftp) || (!$login_result)) {
    echo "FTP connection has failed!\n";
    exit;
} else {
    echo "Connected.\n";
}
ftp_pasv( $ftp, true );

$list = ftp_nlist( $ftp, '/' );
if( empty( $list ) ) {
  echo "File list is empty.\n";

} else {
  foreach( $list as $file ) {
    echo "$file\n";
  }

  $list = ftp_nlist( $ftp, '/ftptest.php' );
  if( ! $list ) {
    echo "\nNot found.\n";
  } else {
    echo "\nFound!\n";
  }


}

// close the FTP connection
ftp_close($ftp);
>

On my server, the output is:

Connected.
/.
/..
/htdocs
/cgi-bin
/logs
/ftptest.php

Not found.

EDIT:
BTW, running pure-ftpd-mysql v1.0.49

Last edited 3 years ago by zippy1970 (previous) (diff)

#11 @costdev
3 years ago

In WP_Filesystem_FTPext::exists()Docs, it calls $list = ftp_nlist( $this->link, $file ), which returns false.

The result is checked against empty( $list ) && $this->is_dir( $file ), which passes the first condition and fails the second.

Then the WP_Filesystem_FTPext::exists() returns ! empty( $list );.

! true === false === the file does not exist, so this is indeed causing a problem here.

Last edited 3 years ago by costdev (previous) (diff)

#12 follow-up: @costdev
3 years ago

Does adding this before the call to ftp_pasv() change the result for you?

ftp_set_option( $ftp, FTP_USEPASVADDRESS, false );

#13 in reply to: ↑ 12 @zippy1970
3 years ago

Replying to costdev:

Does adding this before the call to ftp_pasv() change the result for you?

ftp_set_option( $ftp, FTP_USEPASVADDRESS, false );

No, ftp_nlist() (in class-wp-filesystem-ftpext.php) still returns false on a file.

EDIT:
Sorry, also tried it in my code above- same result.

Last edited 3 years ago by zippy1970 (previous) (diff)

#14 follow-up: @zippy1970
3 years ago

Ok, it turns out to be a "problem" with the new version of Pure-FTPd that was installed with Debian 11. It installs v1.0.49 (previously installed version was v1.0.47) and according to their changelog here, globbing was removed from the NLST command in v1.0.48 (which makes sense since it's actually not allowed according to the RFC).

So it's weird that this problem hasn't been reported numerous times before since everybody who uses the FTP method to update and also uses Pure-FTPd, will have this problem...

The only solution is a rewrite of the code in class-wp-filesystem-ftpext.php, or switching to the direct method of updating. Which sucks because that opens a whole can of worms of security issues.

Last edited 3 years ago by zippy1970 (previous) (diff)

#15 in reply to: ↑ 14 ; follow-up: @ocean90
3 years ago

Replying to zippy1970:

So it's weird that this problem hasn't been reported numerous times before since everybody who uses the FTP method to update and also uses Pure-FTPd, will have this problem...

Sounds like we already have report for it in #51170. Can you confirm?

#16 @zippy1970
3 years ago

In class-wp-filesystem-ftpext.php I replaced the exists() function with my own version (below) and the update succeeded without a hitch.

<?php
        public function exists( $file ) {
          $retval = false;

          $list = ftp_nlist( $this->link, $file );
          if( ! empty( $list ) ) {
            // if ftp_nlist returns *something*, the file or directory exists, on any FTP server
            $retval = true;
          } else {
            // if ftp_nlist returns nothing, either the file/dir doesn't exist or it's a file and
            // the FTP server's NLST command doesn't support globbing (i.e. Pure-FTPD > v1.0.47)
            // Check if it'a file
            if( ftp_size( $this->link, $file ) >= 0 ) {
              $retval = true;
            }
          }
          return $retval;

        }

#17 in reply to: ↑ 15 @zippy1970
3 years ago

Replying to ocean90:

Sounds like we already have report for it in #51170. Can you confirm?

Yes, that's it. If they would have made a comment that this causes updates to fail, that would have saved me a lot of trouble. :)

Apparently they changed the exists() function in the exact same way I did.

#18 @SergeyBiryukov
3 years ago

  • Milestone Awaiting Review deleted
  • Resolution set to duplicate
  • Status changed from new to closed

Thanks for the confirmation! Let's continue in #51170 then, as it already has a patch.

#19 @costdev
3 years ago

  • Keywords needs-testing removed
Note: See TracTickets for help on using tickets.