Files
coolify/app/Services/ProjectIconStorageService.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

82 lines
2.7 KiB
PHP

<?php
namespace App\Services;
use App\Models\Project;
use Illuminate\Http\UploadedFile;
use RuntimeException;
class ProjectIconStorageService extends AvatarStorageService
{
public function storeProject(Project $project, UploadedFile $upload): void
{
$this->storeProjectContents($project, $this->compress($upload));
}
/**
* Stores an icon that is already a compressed JPEG, for example one from a server transfer bundle.
*/
public function storeProjectContents(Project $project, string $contents): void
{
$settings = instanceSettings();
$storageType = $settings->avatar_storage_type === 's3' && $settings->avatar_s3_storage_id ? 's3' : 'local';
$s3StorageId = $storageType === 's3' ? $settings->avatar_s3_storage_id : null;
$disk = $this->disk($storageType, $s3StorageId);
// The uuid is a path segment, so it must not leave the project-icons directory.
if (preg_match('/\A[a-zA-Z0-9_-]+\z/', (string) $project->uuid) !== 1) {
throw new RuntimeException('Unable to store the project icon: the project UUID is not valid.');
}
$path = "project-icons/{$project->uuid}/icon.jpg";
if (! $disk->put($path, $contents)) {
throw new RuntimeException('Unable to store the project icon.');
}
$oldStorageType = $project->icon_storage_type;
$oldS3StorageId = $project->icon_s3_storage_id;
$oldPath = $project->icon_path;
$project->forceFill([
'icon_path' => $path,
'icon_storage_type' => $storageType,
'icon_s3_storage_id' => $s3StorageId,
])->save();
if ($oldPath && ($oldStorageType !== $storageType || $oldS3StorageId !== $s3StorageId)) {
$this->disk($oldStorageType ?? 'local', $oldS3StorageId)->delete($oldPath);
}
}
public function projectContents(Project $project): ?string
{
if (! $project->icon_path) {
return null;
}
try {
$disk = $this->disk($project->icon_storage_type ?? 'local', $project->icon_s3_storage_id);
} catch (RuntimeException) {
return null;
}
return $disk->exists($project->icon_path) ? $disk->get($project->icon_path) : null;
}
public function deleteProject(Project $project): void
{
if ($project->icon_path) {
try {
$this->disk($project->icon_storage_type ?? 'local', $project->icon_s3_storage_id)
->delete($project->icon_path);
} catch (RuntimeException) {
}
}
$project->forceFill([
'icon_path' => null,
'icon_storage_type' => null,
'icon_s3_storage_id' => null,
])->save();
}
}