Files
coolify/tests/Unit/ServerTransfer/ServerTransferBundleTest.php
T
Andras BacsaiandClaude Opus 5.5 cf9f254507 fix(server-transfer): close security holes in import, claim, and archives
Import:
- Check every uuid in a bundle against a strict id format before any
  write, and quote the SSH mux socket and key paths in local shell
  commands. A bundle uuid could run commands in the Coolify container.
- Validate bundle fqdns, git source URLs, paths, networks, cron
  expressions, and database ports; cap the number of items.
- Do not confirm that another team has a uuid.
- Refuse a project icon path with a uuid that is not a plain id.

Claim, finish, and migrate:
- Pass checklist step keys to wire:click with Js::from (stored XSS).
- Link only to https target server URLs.
- Validate target_instance_url in /transfer/complete; never prefill an
  unverified URL for a transfer back; scope the transfer-back token to
  write+deploy for 1 hour.
- Release on an owner marker only when it is newer than the own claim.
- Do not touch apps that only use the server as an additional
  destination when an archive is released or deleted.
- Give Sentinel a new token on claim.
- Add a checklist step to remove the previous instance's SSH key.
- Block a transfer while backups or scheduled tasks run.
- Authorize refreshMigration and PrivateKey checkConnection, clear
  tokens from Livewire state, hide migration log/error from members.

Archives:
- Refuse ip, user, port, private key, and Cloudflare tunnel changes.
- Throttle Enable management and require a free server slot for it and
  for a transfer back.
- Skip cloud provider calls.
- Re-read force_disabled before each SSH command, so running jobs stop.
- Probe the import check target at the resolved address only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 21:02:32 +02:00

95 lines
3.4 KiB
PHP

<?php
use App\Services\ServerTransfer\ServerTransferBundle;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Tests\TestCase;
uses(TestCase::class);
test('wrap adds schema version export id and timestamp', function () {
$bundle = ServerTransferBundle::wrap(['server' => ['uuid' => 'abc']]);
expect($bundle['schema_version'])->toBe(ServerTransferBundle::SCHEMA_VERSION)
->and($bundle['export_id'])->toBeString()->not->toBeEmpty()
->and($bundle['exported_at'])->toBeString()
->and($bundle['server']['uuid'])->toBe('abc');
});
test('validate rejects missing required fields', function () {
$result = ServerTransferBundle::validate([]);
expect($result['valid'])->toBeFalse()
->and($result['errors'])->not->toBeEmpty();
});
test('validate accepts a minimal valid bundle', function () {
$bundle = ServerTransferBundle::wrap([
'private_key' => ['private_key' => "-----BEGIN OPENSSH PRIVATE KEY-----\ntest\n-----END OPENSSH PRIVATE KEY-----"],
'server' => [
'uuid' => 'srv-1',
'name' => 'web',
'ip' => '10.0.0.1',
'port' => 22,
'user' => 'root',
],
'destinations' => [],
'projects' => [],
]);
$result = ServerTransferBundle::validate($bundle);
expect($result['valid'])->toBeTrue()
->and($result['warnings'])->not->toBeEmpty(); // empty destinations warning
});
test('assertValid throws validation exception', function () {
ServerTransferBundle::assertValid(['schema_version' => 99]);
})->throws(ValidationException::class);
function bundleTestMinimalBundle(): array
{
return ServerTransferBundle::wrap([
'private_key' => ['private_key' => 'x'],
'server' => ['uuid' => 's', 'name' => 'n', 'ip' => '1.1.1.1', 'port' => 22, 'user' => 'root'],
'destinations' => [],
'projects' => [],
]);
}
test('validate rejects a bundle from a newer or unknown Coolify version', function (?string $version, string $error) {
$bundle = bundleTestMinimalBundle();
$bundle['coolify_version'] = $version;
$result = ServerTransferBundle::validate($bundle);
expect($result['valid'])->toBeFalse()
->and(implode(' ', $result['errors']))->toContain($error);
})->with([
'newer' => ['99.0.0', 'Upgrade this instance'],
'missing' => [null, 'Missing coolify_version'],
]);
test('validate accepts a bundle from an older Coolify version', function () {
$bundle = bundleTestMinimalBundle();
$bundle['coolify_version'] = '4.0.0';
expect(ServerTransferBundle::validate($bundle)['valid'])->toBeTrue();
});
test('validate accepts Coolify ids and legacy RFC 4122 uuids', function () {
$bundle = bundleTestMinimalBundle();
$bundle['server']['uuid'] = new_public_id();
$bundle['private_key']['uuid'] = (string) Str::uuid();
$bundle['volume_backups'] = [['uuid' => 'abc_DEF-123', 's3_storage_uuids' => [(string) Str::uuid()], 'frequency' => 'daily']];
expect(ServerTransferBundle::validate($bundle)['errors'])->toBe([]);
});
test('validate rejects an unsafe id in an id list', function () {
$bundle = bundleTestMinimalBundle();
$bundle['volume_backups'] = [['s3_storage_uuids' => ['ok', '../../x']]];
expect(ServerTransferBundle::validate($bundle)['errors'])->toContain('volume_backups.0.s3_storage_uuids.1 is not a valid id. An id may only contain letters, digits, hyphens, and underscores (at most 64 characters).');
});