Skip to content

fix(ftp): handle empty port parameter and ensure strictly numeric ports - #63161

Open
bahman026 wants to merge 2 commits into
nextcloud:masterfrom
bahman026:fix/noid/files-external-ftp-empty-port
Open

fix(ftp): handle empty port parameter and ensure strictly numeric ports#63161
bahman026 wants to merge 2 commits into
nextcloud:masterfrom
bahman026:fix/noid/files-external-ftp-empty-port

Conversation

@bahman026

@bahman026 bahman026 commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Creating an FTP/FTPS external storage with an empty Port field ends in an HTTP 500.

FTP::__construct() read the port like this:

$this->port = $parameters['port'] ?? 21;

?? only substitutes null or a missing key. The external storage settings submit an
empty string when the field is left blank, so "" was assigned to $this->port and
passed on to FtpConnection::__construct(bool $secure, string $hostname, int $port, ...).
PHP rejects a non-numeric string for an int parameter, so the request died with a
TypeError:

OCA\Files_External\Lib\Storage\FtpConnection::__construct(): Argument #3 ($port)
must be of type int, string given, called in apps/files_external/lib/Lib/Storage/FTP.php

A numeric string such as "2121" is coerced normally, so explicitly configured ports
were never affected — only an empty (or otherwise non-numeric) value.

Two details make the failure more confusing for users:

  • The storage is already stored when the error happens. UserStoragesController::create()
    saves the configuration first and calls updateStorageStatus() afterwards, and the
    crash happens in that status check. The user gets a 500 and no visible storage, then
    finds the storage after reloading the page.
  • FTP::getConnection(), MountConfig::getBackendStatus() and
    StoragesController::updateStorageStatus() all catch \Exception. A TypeError is an
    \Error, so it passes through them and surfaces as a hard 500 instead of a
    "storage not available" status.

SFTP had exactly the same problem and it was fixed in #58350
(reported in #58324, #58293, #58507 and #58833). FTP.php never received the equivalent
guard. FTP and SFTP are the only backends that read ['port'].

Steps to reproduce

  1. As an administrator, enable Allow users to mount external storage.
  2. As a user, open Personal settings → External storage.
  3. Add a storage with backend FTP and an authentication mechanism whose credentials
    are available (for example global credentials).
  4. Fill in Host, leave Port empty and save.
  5. The request fails with a 500, and the log contains the TypeError shown above.
  6. Reload the page: the storage is there, because it was saved before the crash.

The same steps with SFTP work, thanks to the guard from #58350.

Fix

Fall back to the default port unless the configured value is numeric, mirroring the SFTP
guard:

$parsedPort = $parameters['port'] ?? null;
$this->port = is_numeric($parsedPort) ? (int)$parsedPort : 21;

21 stays the default, so nothing changes for existing configurations. Casting without
the check would not work, because (int)"" is 0 rather than 21.

Tests

apps/files_external/tests/FtpConstructorTest.php covers the constructor for a missing,
empty, null, non-numeric, numeric-string and integer port. The FTP connection is only
opened lazily, so no FTP server is needed. The test is placed in tests/ rather than
tests/Storage/ because tests/phpunit-autotest-external.xml excludes the latter
directory, and a test there would not run in CI.

I verified the whole files_external suite as well as the Storage/FtpTest.php backend
tests against a real FTP server, and confirmed the new test fails without this change.

Side note

The catch (\Exception $e) blocks mentioned above could be \Throwable, so that this
class of error degrades to a storage status instead of a 500. I left that out to keep
this change minimal, and I am happy to open a separate pull request for it.

Checklist

The external storage settings submit an empty string when the Port field
is left blank. `??` only substitutes null or missing values, so `""` was
assigned to $port and passed to FtpConnection::__construct(), whose $port
parameter is typed int. PHP rejects a non-numeric string there, so adding
an FTP storage without a port ended in a TypeError and an HTTP 500 - after
the configuration had already been saved.

Fall back to the default port unless the configured value is numeric,
mirroring the guard added for SFTP in nextcloud#58350.

Signed-off-by: Bahman Jafarzadeh <bja@adlas.at>
@bahman026
bahman026 requested a review from a team as a code owner August 11, 2026 11:28
@bahman026
bahman026 requested review from Altahrim, leftybournes, provokateurin and salmart-dev and removed request for a team August 11, 2026 11:28
@bahman026

Copy link
Copy Markdown
Contributor Author

/backport to stable34

Comment on lines +16 to +20
/**
* The FTP connection is established lazily, so the constructor can be covered
* without a reachable FTP server. The backend tests in Storage/FtpTest.php do
* need one and are run separately.
*/

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.

Suggested change
/**
* The FTP connection is established lazily, so the constructor can be covered
* without a reachable FTP server. The backend tests in Storage/FtpTest.php do
* need one and are run separately.
*/

* without a reachable FTP server. The backend tests in Storage/FtpTest.php do
* need one and are run separately.
*/
class FtpConstructorTest extends TestCase {

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.

FtpTest would be more appropriate as class name

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants